This commit is contained in:
Thibault Deckers 2023-03-25 00:09:13 +01:00
parent ad3edf4458
commit 86b982d270
92 changed files with 573 additions and 435 deletions

View file

@ -33,11 +33,23 @@ import java.util.regex.Pattern
object StorageUtils { object StorageUtils {
private val LOG_TAG = LogUtils.createTag<StorageUtils>() private val LOG_TAG = LogUtils.createTag<StorageUtils>()
// from `DocumentsContract` private const val SCHEME_CONTENT = ContentResolver.SCHEME_CONTENT
// cf DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY
private const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents" private const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents"
// cf DocumentsContract.EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID
private const val EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID = "primary" private const val EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID = "primary"
private const val TREE_URI_ROOT = "content://$EXTERNAL_STORAGE_PROVIDER_AUTHORITY/tree/" private const val TREE_URI_ROOT = "$SCHEME_CONTENT://$EXTERNAL_STORAGE_PROVIDER_AUTHORITY/tree/"
private val MEDIA_STORE_VOLUME_EXTERNAL = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) MediaStore.VOLUME_EXTERNAL else "external"
// TODO TLAD get it from `MediaStore.Images.Media.EXTERNAL_CONTENT_URI`?
private val IMAGE_PATH_ROOT = "/$MEDIA_STORE_VOLUME_EXTERNAL/images/"
// TODO TLAD get it from `MediaStore.Video.Media.EXTERNAL_CONTENT_URI`?
private val VIDEO_PATH_ROOT = "/$MEDIA_STORE_VOLUME_EXTERNAL/video/"
private val UUID_PATTERN = Regex("[A-Fa-f\\d-]+") private val UUID_PATTERN = Regex("[A-Fa-f\\d-]+")
private val TREE_URI_PATH_PATTERN = Pattern.compile("(.*?):(.*)") private val TREE_URI_PATH_PATTERN = Pattern.compile("(.*?):(.*)")
@ -545,7 +557,7 @@ object StorageUtils {
uri ?: return false uri ?: return false
// a URI's authority is [userinfo@]host[:port] // a URI's authority is [userinfo@]host[:port]
// but we only want the host when comparing to Media Store's "authority" // but we only want the host when comparing to Media Store's "authority"
return ContentResolver.SCHEME_CONTENT.equals(uri.scheme, ignoreCase = true) && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true) return SCHEME_CONTENT.equals(uri.scheme, ignoreCase = true) && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)
} }
fun getOriginalUri(context: Context, uri: Uri): Uri { fun getOriginalUri(context: Context, uri: Uri): Uri {
@ -554,7 +566,7 @@ object StorageUtils {
val path = uri.path val path = uri.path
path ?: return uri path ?: return uri
// from Android 11, accessing the original URI for a `file` or `downloads` media content yields a `SecurityException` // from Android 11, accessing the original URI for a `file` or `downloads` media content yields a `SecurityException`
if (path.startsWith("/external/images/") || path.startsWith("/external/video/")) { if (path.startsWith(IMAGE_PATH_ROOT) || path.startsWith(VIDEO_PATH_ROOT)) {
// "Caller must hold ACCESS_MEDIA_LOCATION permission to access original" // "Caller must hold ACCESS_MEDIA_LOCATION permission to access original"
if (context.checkSelfPermission(Manifest.permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED) { if (context.checkSelfPermission(Manifest.permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED) {
return MediaStore.setRequireOriginal(uri) return MediaStore.setRequireOriginal(uri)
@ -611,7 +623,7 @@ object StorageUtils {
return uri return uri
} }
// Build a typical `images` or `videos` content URI from the original content ID. // Build a typical `images` or `video` content URI from the original content ID.
// We cannot safely apply this to a `file` content URI, as it may point to a file not indexed // We cannot safely apply this to a `file` content URI, as it may point to a file not indexed
// by the Media Store (via `.nomedia`), and therefore has no matching image/video content URI. // by the Media Store (via `.nomedia`), and therefore has no matching image/video content URI.
private fun getMediaUriImageVideoUri(uri: Uri, mimeType: String): Uri? { private fun getMediaUriImageVideoUri(uri: Uri, mimeType: String): Uri? {

View file

@ -39,7 +39,7 @@ class AppIconImage extends ImageProvider<AppIconImageKey> {
Future<ui.Codec> _loadAsync(AppIconImageKey key, DecoderBufferCallback decode) async { Future<ui.Codec> _loadAsync(AppIconImageKey key, DecoderBufferCallback decode) async {
try { try {
final bytes = await androidAppService.getAppIcon(key.packageName, key.size); final bytes = await appService.getAppIcon(key.packageName, key.size);
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes.isEmpty ? kTransparentImage : bytes); final buffer = await ui.ImmutableBuffer.fromUint8List(bytes.isEmpty ? kTransparentImage : bytes);
return await decode(buffer); return await decode(buffer);
} catch (error) { } catch (error) {

View file

@ -1 +1,7 @@
enum MoveType { copy, move, export, toBin, fromBin } enum MoveType {
copy,
move,
export,
toBin,
fromBin,
}

View file

@ -0,0 +1,96 @@
import 'package:aves/ref/mime_types.dart';
class AppSupport {
// TODO TLAD [codec] make it dynamic if it depends on OS/lib versions
static const Set<String> undecodableImages = {
MimeTypes.art,
MimeTypes.cdr,
MimeTypes.crw,
MimeTypes.djvu,
MimeTypes.jpeg2000,
MimeTypes.jxl,
MimeTypes.pat,
MimeTypes.pcx,
MimeTypes.pnm,
MimeTypes.psdVnd,
MimeTypes.psdX,
MimeTypes.octetStream,
MimeTypes.zip,
};
static bool canDecode(String mimeType) => !undecodableImages.contains(mimeType);
// 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.
static bool _supportedByBitmapRegionDecoder(String mimeType) => [
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);
static bool canDecodeRegion(String mimeType) => _supportedByBitmapRegionDecoder(mimeType) || mimeType == MimeTypes.tiff;
// `exifinterface` v1.3.3 declared support for DNG, but it strips non-standard Exif tags when saving attributes,
// and DNG requires DNG-specific tags saved along standard Exif. So it was actually breaking DNG files.
static bool canEditExif(String mimeType) {
switch (mimeType.toLowerCase()) {
// as of androidx.exifinterface:exifinterface:1.3.4
case MimeTypes.jpeg:
case MimeTypes.png:
case MimeTypes.webp:
return true;
default:
return false;
}
}
static bool canEditIptc(String mimeType) {
switch (mimeType.toLowerCase()) {
// as of latest PixyMeta
case MimeTypes.jpeg:
case MimeTypes.tiff:
return true;
default:
return false;
}
}
static bool canEditXmp(String mimeType) {
switch (mimeType.toLowerCase()) {
// as of latest PixyMeta
case MimeTypes.gif:
case MimeTypes.jpeg:
case MimeTypes.png:
case MimeTypes.tiff:
return true;
// using `mp4parser`
case MimeTypes.mp4:
return true;
default:
return false;
}
}
static bool canRemoveMetadata(String mimeType) {
switch (mimeType.toLowerCase()) {
// as of latest PixyMeta
case MimeTypes.jpeg:
case MimeTypes.tiff:
return true;
default:
return false;
}
}
}

78
lib/model/apps.dart Normal file
View file

@ -0,0 +1,78 @@
import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
final AppInventory appInventory = AppInventory._private();
class AppInventory {
Set<Package> _packages = {};
List<String> _potentialAppDirs = [];
ValueNotifier<bool> areAppNamesReadyNotifier = ValueNotifier(false);
Iterable<Package> get _launcherPackages => _packages.where((v) => v.categoryLauncher);
AppInventory._private();
Future<void> initAppNames() async {
if (_packages.isEmpty) {
debugPrint('Access installed app inventory');
_packages = await appService.getPackages();
_potentialAppDirs = _launcherPackages.expand((v) => v.potentialDirs).toList();
areAppNamesReadyNotifier.value = true;
}
}
Future<void> resetAppNames() async {
_packages.clear();
_potentialAppDirs.clear();
areAppNamesReadyNotifier.value = false;
}
bool isPotentialAppDir(String dir) => _potentialAppDirs.contains(dir);
String? getAlbumAppPackageName(String albumPath) {
final dir = pContext.split(albumPath).last;
final package = _launcherPackages.firstWhereOrNull((v) => v.potentialDirs.contains(dir));
return package?.packageName;
}
String? getCurrentAppName(String packageName) {
final package = _packages.firstWhereOrNull((v) => v.packageName == packageName);
return package?.currentLabel;
}
}
class Package {
final String packageName;
final String? currentLabel, englishLabel;
final bool categoryLauncher, isSystem;
final Set<String> ownedDirs = {};
Package({
required this.packageName,
required this.currentLabel,
required this.englishLabel,
required this.categoryLauncher,
required this.isSystem,
});
factory Package.fromMap(Map map) {
return Package(
packageName: map['packageName'] ?? '',
currentLabel: map['currentLabel'],
englishLabel: map['englishLabel'],
categoryLauncher: map['categoryLauncher'] ?? false,
isSystem: map['isSystem'] ?? false,
);
}
Set<String> get potentialDirs => [
currentLabel,
englishLabel,
...ownedDirs,
].whereNotNull().toSet();
@override
String toString() => '$runtimeType#${shortHash(this)}{packageName=$packageName, categoryLauncher=$categoryLauncher, isSystem=$isSystem, currentLabel=$currentLabel, englishLabel=$englishLabel, ownedDirs=$ownedDirs}';
}

View file

@ -1,10 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/apps.dart';
import 'package:aves/model/entry/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/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/vaults/vaults.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:collection/collection.dart'; import 'package:collection/collection.dart';
@ -121,7 +123,7 @@ class Covers {
String? effectiveAlbumPackage(String albumPath) { String? effectiveAlbumPackage(String albumPath) {
final filterPackage = of(AlbumFilter(albumPath, null))?.item2; final filterPackage = of(AlbumFilter(albumPath, null))?.item2;
return filterPackage ?? androidFileUtils.getAlbumAppPackageName(albumPath); return filterPackage ?? appInventory.getAlbumAppPackageName(albumPath);
} }
// import/export // import/export

View file

@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:aves/model/storage/relative_dir.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:collection/collection.dart'; import 'package:collection/collection.dart';
final entryDirRepo = EntryDirRepo._private(); final entryDirRepo = EntryDirRepo._private();

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/model/entry/cache.dart'; import 'package:aves/model/entry/cache.dart';
@ -7,13 +6,12 @@ import 'package:aves/model/entry/dirs.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/source/trash.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:aves/theme/format.dart'; import 'package:aves/theme/format.dart';
import 'package:aves_utils/aves_utils.dart';
import 'package:aves/utils/time_utils.dart'; import 'package:aves/utils/time_utils.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.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';
@ -80,10 +78,6 @@ class AvesEntry with AvesEntryBase {
this.durationMillis = durationMillis; this.durationMillis = durationMillis;
} }
bool get canDecode => !MimeTypes.undecodableImages.contains(mimeType);
bool get canHaveAlpha => MimeTypes.alphaImages.contains(mimeType);
AvesEntry copyWith({ AvesEntry copyWith({
int? id, int? id,
String? uri, String? uri,
@ -225,15 +219,6 @@ class AvesEntry with AvesEntryBase {
return _extension; return _extension;
} }
String? get storagePath => trashed ? trashDetails?.path : path;
String? get storageDirectory => trashed ? pContext.dirname(trashDetails!.path) : directory;
bool get isMissingAtPath {
final _storagePath = storagePath;
return _storagePath != null && !File(_storagePath).existsSync();
}
// the MIME type reported by the Media Store is unreliable // the MIME type reported by the Media Store is unreliable
// 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;
@ -323,18 +308,6 @@ class AvesEntry with AvesEntryBase {
return _durationText!; return _durationText!;
} }
bool get isExpiredTrash {
final dateMillis = trashDetails?.dateMillis;
if (dateMillis == null) return false;
return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).isBefore(DateTime.now());
}
int? get trashDaysLeft {
final dateMillis = trashDetails?.dateMillis;
if (dateMillis == null) return null;
return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).difference(DateTime.now()).inDays;
}
// returns whether this entry has GPS coordinates // returns whether this entry has GPS coordinates
// (0, 0) coordinates are considered invalid, as it is likely a default value // (0, 0) coordinates are considered invalid, as it is likely a default value
bool get hasGps => (_catalogMetadata?.latitude ?? 0) != 0 || (_catalogMetadata?.longitude ?? 0) != 0; bool get hasGps => (_catalogMetadata?.latitude ?? 0) != 0 || (_catalogMetadata?.longitude ?? 0) != 0;

View file

@ -1,65 +1,111 @@
import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/model/app/support.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/trash.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/ref/unicode.dart'; import 'package:aves/ref/unicode.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/text.dart'; import 'package:aves/theme/text.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
extension ExtraAvesEntryProps on AvesEntry { extension ExtraAvesEntryProps on AvesEntry {
// type
String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*'); String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*');
bool get canHaveAlpha => MimeTypes.canHaveAlpha(mimeType);
bool get isSvg => mimeType == MimeTypes.svg; 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 isRaw => MimeTypes.isRaw(mimeType);
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 isImage => MimeTypes.isImage(mimeType);
bool get isVideo => MimeTypes.isVideo(mimeType); bool get isVideo => MimeTypes.isVideo(mimeType);
// size
bool get useTiles => canDecodeRegion && (width > 4096 || height > 4096);
bool get isSized => width > 0 && height > 0;
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;
}
// text
String get resolutionText {
final ws = width;
final hs = height;
return isRotated ? '$hs${AText.resolutionSeparator}$ws' : '$ws${AText.resolutionSeparator}$hs';
}
String get aspectRatioText {
const separator = UniChars.ratio;
if (width > 0 && height > 0) {
final gcd = width.gcd(height);
final w = width ~/ gcd;
final h = height ~/ gcd;
return isRotated ? '$h$separator$w' : '$w$separator$h';
} else {
return '?$separator?';
}
}
// catalog
bool get isAnimated => catalogMetadata?.isAnimated ?? false; bool get isAnimated => catalogMetadata?.isAnimated ?? false;
bool get isGeotiff => catalogMetadata?.isGeotiff ?? false; bool get isGeotiff => catalogMetadata?.isGeotiff ?? false;
bool get is360 => catalogMetadata?.is360 ?? false; bool get is360 => catalogMetadata?.is360 ?? false;
bool get isMediaStoreContent => uri.startsWith('content://media/'); // trash
bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains); bool get isExpiredTrash {
final dateMillis = trashDetails?.dateMillis;
if (dateMillis == null) return false;
return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).isBefore(DateTime.now());
}
bool get isVaultContent => path?.startsWith(androidFileUtils.vaultRoot) ?? false; int? get trashDaysLeft {
final dateMillis = trashDetails?.dateMillis;
if (dateMillis == null) return null;
return DateTime.fromMillisecondsSinceEpoch(dateMillis).add(TrashMixin.binKeepDuration).difference(DateTime.now()).inDays;
}
bool get canEdit => !settings.isReadOnly && path != null && !trashed && (isMediaStoreContent || isVaultContent); // storage
String? get storageDirectory => trashed ? pContext.dirname(trashDetails!.path) : directory;
bool get isMissingAtPath {
final _storagePath = trashed ? trashDetails?.path : path;
return _storagePath != null && !File(_storagePath).existsSync();
}
// providers
bool get _isVaultContent => path?.startsWith(androidFileUtils.vaultRoot) ?? false;
bool get _isMediaStoreContent => uri.startsWith(AndroidFileUtils.mediaStoreUriRoot);
bool get isMediaStoreMediaContent => _isMediaStoreContent && AndroidFileUtils.mediaUriPathRoots.any(uri.contains);
// edition
bool get canEdit => !settings.isReadOnly && path != null && !trashed && (_isMediaStoreContent || _isVaultContent);
bool get canEditDate => canEdit && (canEditExif || canEditXmp); bool get canEditDate => canEdit && (canEditExif || canEditXmp);
@ -75,44 +121,17 @@ extension ExtraAvesEntryProps on AvesEntry {
bool get canFlip => canEdit && canEditExif; bool get canFlip => canEdit && canEditExif;
bool get canEditExif => MimeTypes.canEditExif(mimeType); // app support
bool get canEditIptc => MimeTypes.canEditIptc(mimeType); bool get canDecode => AppSupport.canDecode(mimeType);
bool get canEditXmp => MimeTypes.canEditXmp(mimeType); bool get canDecodeRegion => AppSupport.canDecodeRegion(mimeType) && !isAnimated;
bool get canRemoveMetadata => MimeTypes.canRemoveMetadata(mimeType); bool get canEditExif => AppSupport.canEditExif(mimeType);
bool get isSized => width > 0 && height > 0; bool get canEditIptc => AppSupport.canEditIptc(mimeType);
String get resolutionText { bool get canEditXmp => AppSupport.canEditXmp(mimeType);
final ws = width;
final hs = height;
return isRotated ? '$hs${AText.resolutionSeparator}$ws' : '$ws${AText.resolutionSeparator}$hs';
}
String get aspectRatioText { bool get canRemoveMetadata => AppSupport.canRemoveMetadata(mimeType);
if (width > 0 && height > 0) {
final gcd = width.gcd(height);
final w = width ~/ gcd;
final h = height ~/ gcd;
return isRotated ? '$h${UniChars.ratio}$w' : '$w${UniChars.ratio}$h';
} else {
return '?${UniChars.ratio}?';
}
}
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

@ -1,5 +1,6 @@
import 'package:aves/model/entry/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/storage/volume.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:collection/collection.dart'; import 'package:collection/collection.dart';

View file

@ -1,9 +1,9 @@
import 'package:aves/model/covers.dart'; import 'package:aves/model/covers.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/source/enums/enums.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/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';

View file

@ -1,5 +1,5 @@
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/actions/entry_set.dart';
import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/filters/recent.dart';
import 'package:aves/model/naming_pattern.dart'; import 'package:aves/model/naming_pattern.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';

View file

@ -3,8 +3,8 @@ import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:aves/app_flavor.dart'; import 'package:aves/app_flavor.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/actions/entry_set.dart';
import 'package:aves/model/device.dart'; import 'package:aves/model/device.dart';
import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';

View file

@ -2,6 +2,8 @@ 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';
import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/storage/relative_dir.dart';
import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/model/vaults/vaults.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

@ -9,3 +9,14 @@ enum EntrySortFactor { date, name, rating, size }
enum EntryGroupFactor { none, album, month, day } enum EntryGroupFactor { none, album, month, day }
enum TileLayout { mosaic, grid, list } enum TileLayout { mosaic, grid, list }
enum AlbumType {
regular,
vault,
app,
camera,
download,
screenRecordings,
screenshots,
videoCaptures,
}

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.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

@ -0,0 +1,49 @@
import 'package:aves/utils/android_file_utils.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
@immutable
class VolumeRelativeDirectory extends Equatable {
final String volumePath, relativeDir;
@override
List<Object?> get props => [volumePath, relativeDir];
String get dirPath => '$volumePath$relativeDir';
const VolumeRelativeDirectory({
required this.volumePath,
required this.relativeDir,
});
static VolumeRelativeDirectory fromMap(Map map) {
return VolumeRelativeDirectory(
volumePath: map['volumePath'] ?? '',
relativeDir: map['relativeDir'] ?? '',
);
}
Map<String, dynamic> toMap() => {
'volumePath': volumePath,
'relativeDir': relativeDir,
};
// prefer static method over a null returning factory constructor
static VolumeRelativeDirectory? fromPath(String dirPath) {
final volume = androidFileUtils.getStorageVolume(dirPath);
if (volume == null) return null;
final root = volume.path;
final rootLength = root.length;
return VolumeRelativeDirectory(
volumePath: root,
relativeDir: dirPath.length < rootLength ? '' : dirPath.substring(rootLength),
);
}
String getVolumeDescription(BuildContext context) {
final volume = androidFileUtils.storageVolumes.firstWhereOrNull((volume) => volume.path == volumePath);
return volume?.getDescription(context) ?? volumePath;
}
}

View file

@ -0,0 +1,41 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
@immutable
class StorageVolume extends Equatable {
final String? _description;
final String path, state;
final bool isPrimary, isRemovable;
@override
List<Object?> get props => [_description, path, state, isPrimary, isRemovable];
const StorageVolume({
required String? description,
required this.isPrimary,
required this.isRemovable,
required this.path,
required this.state,
}) : _description = description;
String getDescription(BuildContext? context) {
if (_description != null) return _description!;
// ideally, the context should always be provided, but in some cases (e.g. album comparison),
// this would require numerous additional methods to have the context as argument
// for such a minor benefit: fallback volume description on Android < N
if (isPrimary) return context?.l10n.storageVolumeDescriptionFallbackPrimary ?? 'Internal Storage';
return context?.l10n.storageVolumeDescriptionFallbackNonPrimary ?? 'SD card';
}
factory StorageVolume.fromMap(Map map) {
final isPrimary = map['isPrimary'] ?? false;
return StorageVolume(
description: map['description'],
isPrimary: isPrimary,
isRemovable: map['isRemovable'] ?? false,
path: map['path'] ?? '',
state: map['state'] ?? '',
);
}
}

View file

@ -86,22 +86,9 @@ class MimeTypes {
static const Set<String> rawImages = {arw, cr2, crw, dcr, dng, dngX, erf, k25, kdc, mrw, nef, nrw, orf, pef, raf, raw, rw2, sr2, srf, srw, x3f}; static const Set<String> rawImages = {arw, cr2, crw, dcr, dng, dngX, erf, k25, kdc, mrw, nef, nrw, orf, pef, raf, raw, rw2, sr2, srf, srw, x3f};
// TODO TLAD [codec] make it dynamic if it depends on OS/lib versions static bool canHaveAlpha(String mimeType) => MimeTypes.alphaImages.contains(mimeType);
static const Set<String> undecodableImages = {art, cdr, crw, djvu, jpeg2000, jxl, pat, pcx, pnm, psdVnd, psdX, octetStream, zip};
static const Set<String> _knownOpaqueImages = {jpeg}; static bool isRaw(String mimeType) => MimeTypes.rawImages.contains(mimeType);
static const Set<String> _knownVideos = {v3gpp, asf, avi, aviMSVideo, aviVnd, aviXMSVideo, dvd, flv, flvX, mkv, mkvX, mov, movX, mp2p, mp2t, mp2ts, mp4, mpeg, ogv, realVideo, webm, wmv};
static final Set<String> knownMediaTypes = {
anyImage,
..._knownOpaqueImages,
...alphaImages,
...rawImages,
...undecodableImages,
anyVideo,
..._knownVideos,
};
static bool isImage(String mimeType) => mimeType.startsWith('image'); static bool isImage(String mimeType) => mimeType.startsWith('image');
@ -147,56 +134,4 @@ class MimeTypes {
} }
return null; return null;
} }
// `exifinterface` v1.3.3 declared support for DNG, but it strips non-standard Exif tags when saving attributes,
// and DNG requires DNG-specific tags saved along standard Exif. So it was actually breaking DNG files.
static bool canEditExif(String mimeType) {
switch (mimeType.toLowerCase()) {
// as of androidx.exifinterface:exifinterface:1.3.4
case jpeg:
case png:
case webp:
return true;
default:
return false;
}
}
static bool canEditIptc(String mimeType) {
switch (mimeType.toLowerCase()) {
// as of latest PixyMeta
case jpeg:
case tiff:
return true;
default:
return false;
}
}
static bool canEditXmp(String mimeType) {
switch (mimeType.toLowerCase()) {
// as of latest PixyMeta
case gif:
case jpeg:
case png:
case tiff:
return true;
// using `mp4parser`
case mp4:
return true;
default:
return false;
}
}
static bool canRemoveMetadata(String mimeType) {
switch (mimeType.toLowerCase()) {
// as of latest PixyMeta
case jpeg:
case tiff:
return true;
default:
return false;
}
}
} }

View file

@ -1,14 +1,14 @@
import 'package:aves/model/apps.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/props.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/math_utils.dart'; import 'package:aves/utils/math_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
abstract class AndroidAppService { abstract class AppService {
Future<Set<Package>> getPackages(); Future<Set<Package>> getPackages();
Future<Uint8List> getAppIcon(String packageName, double size); Future<Uint8List> getAppIcon(String packageName, double size);
@ -30,7 +30,7 @@ abstract class AndroidAppService {
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri}); Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri});
} }
class PlatformAndroidAppService implements AndroidAppService { class PlatformAppService implements AppService {
static const _platform = MethodChannel('deckers.thibault/aves/app'); static const _platform = MethodChannel('deckers.thibault/aves/app');
static final _knownAppDirs = { static final _knownAppDirs = {

View file

@ -3,7 +3,7 @@ import 'package:aves/model/db/db_metadata.dart';
import 'package:aves/model/db/db_metadata_sqflite.dart'; import 'package:aves/model/db/db_metadata_sqflite.dart';
import 'package:aves/model/settings/store/store.dart'; import 'package:aves/model/settings/store/store.dart';
import 'package:aves/model/settings/store/store_shared_pref.dart'; import 'package:aves/model/settings/store/store_shared_pref.dart';
import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/app_service.dart';
import 'package:aves/services/device_service.dart'; import 'package:aves/services/device_service.dart';
import 'package:aves/services/media/embedded_data_service.dart'; import 'package:aves/services/media/embedded_data_service.dart';
import 'package:aves/services/media/media_edit_service.dart'; import 'package:aves/services/media/media_edit_service.dart';
@ -31,7 +31,7 @@ final p.Context pContext = getIt<p.Context>();
final AvesAvailability availability = getIt<AvesAvailability>(); final AvesAvailability availability = getIt<AvesAvailability>();
final MetadataDb metadataDb = getIt<MetadataDb>(); final MetadataDb metadataDb = getIt<MetadataDb>();
final AndroidAppService androidAppService = getIt<AndroidAppService>(); final AppService appService = getIt<AppService>();
final DeviceService deviceService = getIt<DeviceService>(); final DeviceService deviceService = getIt<DeviceService>();
final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>(); final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>();
final MediaEditService mediaEditService = getIt<MediaEditService>(); final MediaEditService mediaEditService = getIt<MediaEditService>();
@ -51,7 +51,7 @@ void initPlatformServices() {
getIt.registerLazySingleton<AvesAvailability>(LiveAvesAvailability.new); getIt.registerLazySingleton<AvesAvailability>(LiveAvesAvailability.new);
getIt.registerLazySingleton<MetadataDb>(SqfliteMetadataDb.new); getIt.registerLazySingleton<MetadataDb>(SqfliteMetadataDb.new);
getIt.registerLazySingleton<AndroidAppService>(PlatformAndroidAppService.new); getIt.registerLazySingleton<AppService>(PlatformAppService.new);
getIt.registerLazySingleton<DeviceService>(PlatformDeviceService.new); getIt.registerLazySingleton<DeviceService>(PlatformDeviceService.new);
getIt.registerLazySingleton<EmbeddedDataService>(PlatformEmbeddedDataService.new); getIt.registerLazySingleton<EmbeddedDataService>(PlatformEmbeddedDataService.new);
getIt.registerLazySingleton<MediaEditService>(PlatformMediaEditService.new); getIt.registerLazySingleton<MediaEditService>(PlatformMediaEditService.new);

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/model/app/support.dart';
import 'package:aves/model/entry/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';
@ -152,7 +153,7 @@ class PlatformMediaFetchService implements MediaFetchService {
// `await` here, so that `completeError` will be caught below // `await` here, so that `completeError` will be caught below
return await completer.future; return await completer.future;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { if (_isUnknownVisual(mimeType)) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
} }
@ -191,7 +192,7 @@ class PlatformMediaFetchService implements MediaFetchService {
}); });
if (result != null) return result as Uint8List; if (result != null) return result as Uint8List;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { if (_isUnknownVisual(mimeType)) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
} }
@ -231,7 +232,7 @@ class PlatformMediaFetchService implements MediaFetchService {
}); });
if (result != null) return result as Uint8List; if (result != null) return result as Uint8List;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { if (_isUnknownVisual(mimeType)) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
} }
@ -259,4 +260,47 @@ class PlatformMediaFetchService implements MediaFetchService {
@override @override
Future<T>? resumeLoading<T>(Object taskKey) => servicePolicy.resume<T>(taskKey); Future<T>? resumeLoading<T>(Object taskKey) => servicePolicy.resume<T>(taskKey);
// convenience methods
bool _isUnknownVisual(String mimeType) => !_knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType);
static const Set<String> _knownOpaqueImages = {
MimeTypes.jpeg,
};
static const Set<String> _knownVideos = {
MimeTypes.v3gpp,
MimeTypes.asf,
MimeTypes.avi,
MimeTypes.aviMSVideo,
MimeTypes.aviVnd,
MimeTypes.aviXMSVideo,
MimeTypes.dvd,
MimeTypes.flv,
MimeTypes.flvX,
MimeTypes.mkv,
MimeTypes.mkvX,
MimeTypes.mov,
MimeTypes.movX,
MimeTypes.mp2p,
MimeTypes.mp2t,
MimeTypes.mp2ts,
MimeTypes.mp4,
MimeTypes.mpeg,
MimeTypes.ogv,
MimeTypes.realVideo,
MimeTypes.webm,
MimeTypes.wmv,
};
static final Set<String> _knownMediaTypes = {
MimeTypes.anyImage,
..._knownOpaqueImages,
...MimeTypes.alphaImages,
...MimeTypes.rawImages,
...AppSupport.undecodableImages,
MimeTypes.anyVideo,
..._knownVideos,
};
} }

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.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/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,8 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/storage/relative_dir.dart';
import 'package:aves/model/storage/volume.dart';
import 'package:aves/services/common/output_buffer.dart'; import 'package:aves/services/common/output_buffer.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: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,28 +1,34 @@
import 'package:aves/model/apps.dart';
import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/storage/volume.dart';
import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/model/vaults/vaults.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:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); final AndroidFileUtils androidFileUtils = AndroidFileUtils._private();
class AndroidFileUtils { class AndroidFileUtils {
// cf https://developer.android.com/reference/android/content/ContentResolver#SCHEME_CONTENT
static const contentScheme = 'content';
// cf https://developer.android.com/reference/android/provider/MediaStore#AUTHORITY
static const mediaStoreAuthority = 'media';
// cf https://developer.android.com/reference/android/provider/MediaStore#VOLUME_EXTERNAL
static const externalVolume = 'external';
static const mediaStoreUriRoot = '$contentScheme://$mediaStoreAuthority/';
static const mediaUriPathRoots = {'/$externalVolume/images/', '/$externalVolume/video/'};
static const String trashDirPath = '#trash'; static const String trashDirPath = '#trash';
late final String separator, vaultRoot, primaryStorage; late final String separator, vaultRoot, primaryStorage;
late final String dcimPath, downloadPath, moviesPath, picturesPath, avesVideoCapturesPath; late final String dcimPath, downloadPath, moviesPath, picturesPath, avesVideoCapturesPath;
late final Set<String> videoCapturesPaths; late final Set<String> videoCapturesPaths;
Set<StorageVolume> storageVolumes = {}; Set<StorageVolume> storageVolumes = {};
Set<Package> _packages = {};
List<String> _potentialAppDirs = [];
bool _initialized = false; bool _initialized = false;
ValueNotifier<bool> areAppNamesReadyNotifier = ValueNotifier(false);
Iterable<Package> get _launcherPackages => _packages.where((package) => package.categoryLauncher);
AndroidFileUtils._private(); AndroidFileUtils._private();
Future<void> init() async { Future<void> init() async {
@ -58,21 +64,6 @@ class AndroidFileUtils {
} }
} }
Future<void> initAppNames() async {
if (_packages.isEmpty) {
debugPrint('Access installed app inventory');
_packages = await androidAppService.getPackages();
_potentialAppDirs = _launcherPackages.expand((package) => package.potentialDirs).toList();
areAppNamesReadyNotifier.value = true;
}
}
Future<void> resetAppNames() async {
_packages.clear();
_potentialAppDirs.clear();
areAppNamesReadyNotifier.value = false;
}
bool isCameraPath(String path) => path.startsWith(dcimPath) && (path.endsWith('${separator}Camera') || path.endsWith('${separator}100ANDRO')); bool isCameraPath(String path) => path.startsWith(dcimPath) && (path.endsWith('${separator}Camera') || path.endsWith('${separator}100ANDRO'));
bool isScreenshotsPath(String path) => (path.startsWith(dcimPath) || path.startsWith(picturesPath)) && path.endsWith('${separator}Screenshots'); bool isScreenshotsPath(String path) => (path.startsWith(dcimPath) || path.startsWith(picturesPath)) && path.endsWith('${separator}Screenshots');
@ -103,147 +94,8 @@ class AndroidFileUtils {
if (isVideoCapturesPath(dirPath)) return AlbumType.videoCaptures; if (isVideoCapturesPath(dirPath)) return AlbumType.videoCaptures;
final dir = pContext.split(dirPath).last; final dir = pContext.split(dirPath).last;
if (dirPath.startsWith(primaryStorage) && _potentialAppDirs.contains(dir)) return AlbumType.app; if (dirPath.startsWith(primaryStorage) && appInventory.isPotentialAppDir(dir)) return AlbumType.app;
return AlbumType.regular; return AlbumType.regular;
} }
String? getAlbumAppPackageName(String albumPath) {
final dir = pContext.split(albumPath).last;
final package = _launcherPackages.firstWhereOrNull((package) => package.potentialDirs.contains(dir));
return package?.packageName;
}
String? getCurrentAppName(String packageName) {
final package = _packages.firstWhereOrNull((package) => package.packageName == packageName);
return package?.currentLabel;
}
}
enum AlbumType {
regular,
vault,
app,
camera,
download,
screenRecordings,
screenshots,
videoCaptures,
}
class Package {
final String packageName;
final String? currentLabel, englishLabel;
final bool categoryLauncher, isSystem;
final Set<String> ownedDirs = {};
Package({
required this.packageName,
required this.currentLabel,
required this.englishLabel,
required this.categoryLauncher,
required this.isSystem,
});
factory Package.fromMap(Map map) {
return Package(
packageName: map['packageName'] ?? '',
currentLabel: map['currentLabel'],
englishLabel: map['englishLabel'],
categoryLauncher: map['categoryLauncher'] ?? false,
isSystem: map['isSystem'] ?? false,
);
}
Set<String> get potentialDirs => [
currentLabel,
englishLabel,
...ownedDirs,
].whereNotNull().toSet();
@override
String toString() => '$runtimeType#${shortHash(this)}{packageName=$packageName, categoryLauncher=$categoryLauncher, isSystem=$isSystem, currentLabel=$currentLabel, englishLabel=$englishLabel, ownedDirs=$ownedDirs}';
}
@immutable
class StorageVolume extends Equatable {
final String? _description;
final String path, state;
final bool isPrimary, isRemovable;
@override
List<Object?> get props => [_description, path, state, isPrimary, isRemovable];
const StorageVolume({
required String? description,
required this.isPrimary,
required this.isRemovable,
required this.path,
required this.state,
}) : _description = description;
String getDescription(BuildContext? context) {
if (_description != null) return _description!;
// ideally, the context should always be provided, but in some cases (e.g. album comparison),
// this would require numerous additional methods to have the context as argument
// for such a minor benefit: fallback volume description on Android < N
if (isPrimary) return context?.l10n.storageVolumeDescriptionFallbackPrimary ?? 'Internal Storage';
return context?.l10n.storageVolumeDescriptionFallbackNonPrimary ?? 'SD card';
}
factory StorageVolume.fromMap(Map map) {
final isPrimary = map['isPrimary'] ?? false;
return StorageVolume(
description: map['description'],
isPrimary: isPrimary,
isRemovable: map['isRemovable'] ?? false,
path: map['path'] ?? '',
state: map['state'] ?? '',
);
}
}
@immutable
class VolumeRelativeDirectory extends Equatable {
final String volumePath, relativeDir;
@override
List<Object?> get props => [volumePath, relativeDir];
String get dirPath => '$volumePath$relativeDir';
const VolumeRelativeDirectory({
required this.volumePath,
required this.relativeDir,
});
static VolumeRelativeDirectory fromMap(Map map) {
return VolumeRelativeDirectory(
volumePath: map['volumePath'] ?? '',
relativeDir: map['relativeDir'] ?? '',
);
}
Map<String, dynamic> toMap() => {
'volumePath': volumePath,
'relativeDir': relativeDir,
};
// prefer static method over a null returning factory constructor
static VolumeRelativeDirectory? fromPath(String dirPath) {
final volume = androidFileUtils.getStorageVolume(dirPath);
if (volume == null) return null;
final root = volume.path;
final rootLength = root.length;
return VolumeRelativeDirectory(
volumePath: root,
relativeDir: dirPath.length < rootLength ? '' : dirPath.substring(rootLength),
);
}
String getVolumeDescription(BuildContext context) {
final volume = androidFileUtils.storageVolumes.firstWhereOrNull((volume) => volume.path == volumePath);
return volume?.getDescription(context) ?? volumePath;
}
} }

View file

@ -24,7 +24,7 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/theme/styles.dart'; import 'package:aves/theme/styles.dart';
import 'package:aves/theme/themes.dart'; import 'package:aves/theme/themes.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/model/apps.dart';
import 'package:aves/utils/debouncer.dart'; import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/collection/collection_grid.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
@ -493,9 +493,9 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
void _monitorSettings() { void _monitorSettings() {
void applyIsInstalledAppAccessAllowed() { void applyIsInstalledAppAccessAllowed() {
if (settings.isInstalledAppAccessAllowed) { if (settings.isInstalledAppAccessAllowed) {
androidFileUtils.initAppNames(); appInventory.initAppNames();
} else { } else {
androidFileUtils.resetAppNames(); appInventory.resetAppNames();
} }
} }

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/actions/entry_set_actions.dart'; import 'package:aves/model/actions/entry_set.dart';
import 'package:aves/model/entry/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';

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/actions/entry_set_actions.dart'; import 'package:aves/model/actions/entry_set.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/entry.dart'; import 'package:aves/model/entry/entry.dart';
@ -20,7 +20,7 @@ import 'package:aves/model/source/analysis_controller.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/vaults/vaults.dart'; import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/app_service.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';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
@ -264,7 +264,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
Future<void> _share(BuildContext context) async { Future<void> _share(BuildContext context) async {
final entries = _getTargetItems(context); final entries = _getTargetItems(context);
try { try {
if (!await androidAppService.shareEntries(entries)) { if (!await appService.shareEntries(entries)) {
await showNoMatchingAppDialog(context); await showNoMatchingAppDialog(context);
} }
} on TooManyItemsException catch (_) { } on TooManyItemsException catch (_) {
@ -741,7 +741,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
final name = result.item2; final name = result.item2;
if (name.isEmpty) return; if (name.isEmpty) return;
await androidAppService.pinToHomeScreen(name, coverEntry, filters: filters); await appService.pinToHomeScreen(name, coverEntry, filters: filters);
if (!device.showPinShortcutFeedback) { if (!device.showPinShortcutFeedback) {
showFeedback(context, context.l10n.genericSuccessFeedback); showFeedback(context, context.l10n.genericSuccessFeedback);
} }

View file

@ -1,6 +1,7 @@
import 'package:aves/model/covers.dart'; import 'package:aves/model/covers.dart';
import 'package:aves/model/entry/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/enums/enums.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';
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/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.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/rate_chooser.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/rate_chooser.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View file

@ -1,5 +1,5 @@
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/actions/share_actions.dart'; import 'package:aves/model/actions/share.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.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';

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/actions/share_actions.dart'; import 'package:aves/model/actions/share.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart';
import 'package:aves/widgets/common/basic/popup/menu_row.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';

View file

@ -1,6 +1,7 @@
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/storage/relative_dir.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/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';

View file

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/storage/volume.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

@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/actions/chip.dart';
import 'package:aves/model/covers.dart'; import 'package:aves/model/covers.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';

View file

@ -3,9 +3,9 @@ import 'package:aves/model/covers.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/grid/theme.dart'; import 'package:aves/widgets/common/grid/theme.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/actions/map_actions.dart'; import 'package:aves/model/actions/map.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/actions/map_actions.dart'; import 'package:aves/model/actions/map.dart';
import 'package:aves/model/settings/enums/l10n.dart'; import 'package:aves/model/settings/enums/l10n.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';

View file

@ -1,6 +1,6 @@
import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/image_providers/app_icon_image_provider.dart';
import 'package:aves/model/apps.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/widgets/common/basic/query_bar.dart'; import 'package:aves/widgets/common/basic/query_bar.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/common.dart';
@ -23,7 +23,7 @@ class _DebugAndroidAppSectionState extends State<DebugAndroidAppSection> with Au
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loader = androidAppService.getPackages(); _loader = appService.getPackages();
} }
@override @override

View file

@ -1,3 +1,4 @@
import 'package:aves/model/app/support.dart';
import 'package:aves/model/entry/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/model/metadata/enums/length_unit.dart'; import 'package:aves/model/metadata/enums/length_unit.dart';
@ -205,7 +206,7 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
valueListenable: _mimeTypeNotifier, valueListenable: _mimeTypeNotifier,
builder: (context, mimeType, child) { builder: (context, mimeType, child) {
Widget child; Widget child;
if (MimeTypes.canEditExif(mimeType) || MimeTypes.canEditIptc(mimeType) || MimeTypes.canEditXmp(mimeType)) { if (AppSupport.canEditExif(mimeType) || AppSupport.canEditIptc(mimeType) || AppSupport.canEditXmp(mimeType)) {
child = SwitchListTile( child = SwitchListTile(
value: _writeMetadata, value: _writeMetadata,
onChanged: (v) => setState(() => _writeMetadata = v), onChanged: (v) => setState(() => _writeMetadata = v),

View file

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:aves/model/storage/volume.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';

View file

@ -1,5 +1,5 @@
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/chip_set_actions.dart'; import 'package:aves/model/actions/chip_set.dart';
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.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';

View file

@ -1,7 +1,7 @@
import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/image_providers/app_icon_image_provider.dart';
import 'package:aves/model/apps.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/basic/list_tiles/reselectable_radio.dart'; import 'package:aves/widgets/common/basic/list_tiles/reselectable_radio.dart';
import 'package:aves/widgets/common/basic/query_bar.dart'; import 'package:aves/widgets/common/basic/query_bar.dart';
import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/scaffold.dart';
@ -34,7 +34,7 @@ class _AppPickPageState extends State<AppPickPage> {
void initState() { void initState() {
super.initState(); super.initState();
_selectedValue = widget.initialValue; _selectedValue = widget.initialValue;
_loader = androidAppService.getPackages(); _loader = appService.getPackages();
} }
@override @override

View file

@ -7,6 +7,7 @@ import 'package:aves/model/source/album.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';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/model/apps.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';
import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/identity/empty.dart';
@ -35,7 +36,7 @@ class AlbumListPage extends StatelessWidget {
}, },
builder: (context, s, child) { builder: (context, s, child) {
return ValueListenableBuilder<bool>( return ValueListenableBuilder<bool>(
valueListenable: androidFileUtils.areAppNamesReadyNotifier, valueListenable: appInventory.areAppNamesReadyNotifier,
builder: (context, areAppNamesReady, child) { builder: (context, areAppNamesReady, child) {
return StreamBuilder( return StreamBuilder(
stream: source.eventBus.on<AlbumsChangedEvent>(), stream: source.eventBus.on<AlbumsChangedEvent>(),

View file

@ -1,7 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/chip_set_actions.dart'; import 'package:aves/model/actions/chip_set.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/entry.dart'; import 'package:aves/model/entry/entry.dart';
@ -14,13 +14,13 @@ 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/enums/enums.dart';
import 'package:aves/model/source/enums/view.dart'; import 'package:aves/model/source/enums/view.dart';
import 'package:aves/model/storage/relative_dir.dart';
import 'package:aves/model/vaults/details.dart'; import 'package:aves/model/vaults/details.dart';
import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/model/vaults/vaults.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';
import 'package:aves/services/media/enums.dart'; import 'package:aves/services/media/enums.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/action_mixins/entry_storage.dart'; import 'package:aves/widgets/common/action_mixins/entry_storage.dart';
import 'package:aves/widgets/common/action_mixins/vault_aware.dart'; import 'package:aves/widgets/common/action_mixins/vault_aware.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/actions/chip.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';

View file

@ -1,5 +1,5 @@
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/chip_set_actions.dart'; import 'package:aves/model/actions/chip_set.dart';
import 'package:aves/model/covers.dart'; import 'package:aves/model/covers.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.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/actions/chip_set_actions.dart'; import 'package:aves/model/actions/chip_set.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/query.dart'; import 'package:aves/model/query.dart';
import 'package:aves/model/selection.dart'; import 'package:aves/model/selection.dart';

View file

@ -13,6 +13,7 @@ import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/theme/text.dart'; import 'package:aves/theme/text.dart';
import 'package:aves/model/apps.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';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
@ -108,7 +109,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
if (_filter is AlbumFilter) { if (_filter is AlbumFilter) {
// when we asynchronously fetch installed app names, // when we asynchronously fetch installed app names,
// album filters themselves do not change, but decoration derived from it does // album filters themselves do not change, but decoration derived from it does
chipKey = ValueKey(androidFileUtils.areAppNamesReadyNotifier.value); chipKey = ValueKey(appInventory.areAppNamesReadyNotifier.value);
} }
return AvesFilterChip( return AvesFilterChip(
key: chipKey, key: chipKey,

View file

@ -1,6 +1,6 @@
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/model/storage/volume.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.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';
import 'package:aves/widgets/filter_grids/common/enums.dart'; import 'package:aves/widgets/filter_grids/common/enums.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';

View file

@ -17,6 +17,7 @@ import 'package:aves/services/common/services.dart';
import 'package:aves/services/global_search.dart'; import 'package:aves/services/global_search.dart';
import 'package:aves/services/intent_service.dart'; import 'package:aves/services/intent_service.dart';
import 'package:aves/services/widget_service.dart'; import 'package:aves/services/widget_service.dart';
import 'package:aves/model/apps.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/scaffold.dart';
@ -107,7 +108,7 @@ class _HomePageState extends State<HomePage> {
await androidFileUtils.init(); await androidFileUtils.init();
if (!{actionScreenSaver, actionSetWallpaper}.contains(intentAction) && settings.isInstalledAppAccessAllowed) { if (!{actionScreenSaver, actionSetWallpaper}.contains(intentAction) && settings.isInstalledAppAccessAllowed) {
unawaited(androidFileUtils.initAppNames()); unawaited(appInventory.initAppNames());
} }
if (intentData.isNotEmpty) { if (intentData.isNotEmpty) {

View file

@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/map_actions.dart'; import 'package:aves/model/actions/map.dart';
import 'package:aves/model/actions/map_cluster_actions.dart'; import 'package:aves/model/actions/map_cluster.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/location.dart'; import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/filters/coordinate.dart'; import 'package:aves/model/filters/coordinate.dart';

View file

@ -6,6 +6,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/album.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/location/country.dart'; import 'package:aves/model/source/location/country.dart';
import 'package:aves/model/source/location/place.dart'; import 'package:aves/model/source/location/place.dart';
import 'package:aves/model/source/tag.dart'; import 'package:aves/model/source/tag.dart';

View file

@ -1,6 +1,6 @@
import 'package:aves/model/storage/relative_dir.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CrumbLine extends StatefulWidget { class CrumbLine extends StatefulWidget {

View file

@ -1,6 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/storage/relative_dir.dart';
import 'package:aves/model/storage/volume.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';

View file

@ -2,7 +2,7 @@ import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:aves/model/actions/settings_actions.dart'; import 'package:aves/model/actions/settings.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/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/actions/entry_set.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.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:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart'; import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart';

View file

@ -2,9 +2,9 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/actions/share_actions.dart'; import 'package:aves/model/actions/share.dart';
import 'package:aves/model/device.dart'; import 'package:aves/model/device.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/favourites.dart'; import 'package:aves/model/entry/extensions/favourites.dart';
@ -188,7 +188,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
_addShortcut(context, targetEntry); _addShortcut(context, targetEntry);
break; break;
case EntryAction.copyToClipboard: case EntryAction.copyToClipboard:
androidAppService.copyToClipboard(targetEntry.uri, targetEntry.bestTitle).then((success) { appService.copyToClipboard(targetEntry.uri, targetEntry.bestTitle).then((success) {
showFeedback(context, success ? context.l10n.genericSuccessFeedback : context.l10n.genericFailureFeedback); showFeedback(context, success ? context.l10n.genericSuccessFeedback : context.l10n.genericFailureFeedback);
}); });
break; break;
@ -214,7 +214,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
_move(context, targetEntry, moveType: MoveType.move); _move(context, targetEntry, moveType: MoveType.move);
break; break;
case EntryAction.share: case EntryAction.share:
androidAppService.shareEntries({targetEntry}).then((success) { appService.shareEntries({targetEntry}).then((success) {
if (!success) showNoMatchingAppDialog(context); if (!success) showNoMatchingAppDialog(context);
}); });
break; break;
@ -255,22 +255,22 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
} }
break; break;
case EntryAction.edit: case EntryAction.edit:
androidAppService.edit(targetEntry.uri, targetEntry.mimeType).then((success) { appService.edit(targetEntry.uri, targetEntry.mimeType).then((success) {
if (!success) showNoMatchingAppDialog(context); if (!success) showNoMatchingAppDialog(context);
}); });
break; break;
case EntryAction.open: case EntryAction.open:
androidAppService.open(targetEntry.uri, targetEntry.mimeTypeAnySubtype, forceChooser: true).then((success) { appService.open(targetEntry.uri, targetEntry.mimeTypeAnySubtype, forceChooser: true).then((success) {
if (!success) showNoMatchingAppDialog(context); if (!success) showNoMatchingAppDialog(context);
}); });
break; break;
case EntryAction.openMap: case EntryAction.openMap:
androidAppService.openMap(targetEntry.latLng!).then((success) { appService.openMap(targetEntry.latLng!).then((success) {
if (!success) showNoMatchingAppDialog(context); if (!success) showNoMatchingAppDialog(context);
}); });
break; break;
case EntryAction.setAs: case EntryAction.setAs:
androidAppService.setAs(targetEntry.uri, targetEntry.mimeType).then((success) { appService.setAs(targetEntry.uri, targetEntry.mimeType).then((success) {
if (!success) showNoMatchingAppDialog(context); if (!success) showNoMatchingAppDialog(context);
}); });
break; break;
@ -334,7 +334,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
final uri = fields['uri'] as String?; final uri = fields['uri'] as String?;
final mimeType = fields['mimeType'] as String?; final mimeType = fields['mimeType'] as String?;
if (uri != null && mimeType != null) { if (uri != null && mimeType != null) {
await androidAppService.shareSingle(uri, mimeType).then((success) { await appService.shareSingle(uri, mimeType).then((success) {
if (!success) showNoMatchingAppDialog(context); if (!success) showNoMatchingAppDialog(context);
}); });
} }
@ -363,7 +363,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
final name = result.item2; final name = result.item2;
if (name.isEmpty) return; if (name.isEmpty) return;
await androidAppService.pinToHomeScreen(name, targetEntry, uri: targetEntry.uri); await appService.pinToHomeScreen(name, targetEntry, uri: targetEntry.uri);
if (!device.showPinShortcutFeedback) { if (!device.showPinShortcutFeedback) {
showFeedback(context, context.l10n.genericSuccessFeedback); showFeedback(context, context.l10n.genericSuccessFeedback);
} }

View file

@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/actions/events.dart'; import 'package:aves/model/actions/events.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/info.dart'; import 'package:aves/model/entry/extensions/info.dart';

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/location.dart'; import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';
@ -68,7 +68,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
await controller.seekTo(controller.currentPosition + 10000); await controller.seekTo(controller.currentPosition + 10000);
break; break;
case EntryAction.openVideo: case EntryAction.openVideo:
await androidAppService.open(entry.uri, entry.mimeTypeAnySubtype, forceChooser: false).then((success) { await appService.open(entry.uri, entry.mimeTypeAnySubtype, forceChooser: false).then((success) {
if (!success) showNoMatchingAppDialog(context); if (!success) showNoMatchingAppDialog(context);
}); });
break; break;

View file

@ -1,4 +1,4 @@
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class ShowPreviousIntent extends Intent { class ShowPreviousIntent extends Intent {

View file

@ -1,4 +1,4 @@
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/widgets/viewer/controls/intents.dart'; import 'package:aves/widgets/viewer/controls/intents.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';

View file

@ -1,8 +1,8 @@
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/favourites.dart'; import 'package:aves/model/entry/extensions/favourites.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/entry/extensions/images.dart'; import 'package:aves/model/entry/extensions/images.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
@ -130,7 +130,6 @@ class ViewerDebugPage extends StatelessWidget {
'sizeBytes': '${entry.sizeBytes}', 'sizeBytes': '${entry.sizeBytes}',
'isFavourite': '${entry.isFavourite}', 'isFavourite': '${entry.isFavourite}',
'isSvg': '${entry.isSvg}', 'isSvg': '${entry.isSvg}',
'isPhoto': '${entry.isPhoto}',
'isVideo': '${entry.isVideo}', 'isVideo': '${entry.isVideo}',
'isCatalogued': '${entry.isCatalogued}', 'isCatalogued': '${entry.isCatalogued}',
'isAnimated': '${entry.isAnimated}', 'isAnimated': '${entry.isAnimated}',

View file

@ -3,7 +3,7 @@ import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/catalog.dart'; import 'package:aves/model/entry/extensions/catalog.dart';
import 'package:aves/model/entry/extensions/location.dart'; import 'package:aves/model/entry/extensions/location.dart';

View file

@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.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/entry.dart'; import 'package:aves/model/entry/entry.dart';

View file

@ -1,6 +1,7 @@
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/image_providers/app_icon_image_provider.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/apps.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/favourites.dart'; import 'package:aves/model/entry/extensions/favourites.dart';
import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/multipage.dart';
@ -14,10 +15,10 @@ import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/filters/type.dart'; import 'package:aves/model/filters/type.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';
import 'package:aves/ref/mime_types.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/format.dart'; import 'package:aves/theme/format.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/file_utils.dart'; import 'package:aves/utils/file_utils.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/rate_button.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/rate_button.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/tag_button.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/tag_button.dart';
@ -273,9 +274,12 @@ class _BasicInfoState extends State<_BasicInfo> {
AvesEntry get entry => widget.entry; AvesEntry get entry => widget.entry;
int get megaPixels => entry.megaPixels; int get megaPixels => (entry.width * entry.height / 1000000).round();
bool get showMegaPixels => entry.isPhoto && megaPixels > 0; // guess whether this is a photo, according to file type
bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg, MimeTypes.tiff].contains(entry.mimeType) || entry.isRaw;
bool get showMegaPixels => isPhoto && megaPixels > 0;
String get rasterResolutionText => '${entry.resolutionText}${showMegaPixels ? '$megaPixels MP' : ''}'; String get rasterResolutionText => '${entry.resolutionText}${showMegaPixels ? '$megaPixels MP' : ''}';
@ -291,7 +295,7 @@ class _BasicInfoState extends State<_BasicInfo> {
}); });
final isViewerMode = context.read<ValueNotifier<AppMode>>().value == AppMode.view; final isViewerMode = context.read<ValueNotifier<AppMode>>().value == AppMode.view;
if (isViewerMode && settings.isInstalledAppAccessAllowed) { if (isViewerMode && settings.isInstalledAppAccessAllowed) {
_appNameLoader = androidFileUtils.initAppNames(); _appNameLoader = appInventory.initAppNames();
} }
} }
} }
@ -349,7 +353,7 @@ class _BasicInfoState extends State<_BasicInfo> {
InfoValueSpanBuilder _ownerHandler(String? ownerPackage) { InfoValueSpanBuilder _ownerHandler(String? ownerPackage) {
if (ownerPackage == null) return (context, key, value) => []; if (ownerPackage == null) return (context, key, value) => [];
final appName = androidFileUtils.getCurrentAppName(ownerPackage) ?? ownerPackage; final appName = appInventory.getCurrentAppName(ownerPackage) ?? ownerPackage;
return (context, key, value) => [ return (context, key, value) => [
WidgetSpan( WidgetSpan(
alignment: PlaceholderAlignment.middle, alignment: PlaceholderAlignment.middle,

View file

@ -62,10 +62,10 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin {
final uri = fields['uri']!; final uri = fields['uri']!;
if (!MimeTypes.isImage(mimeType) && !MimeTypes.isVideo(mimeType)) { if (!MimeTypes.isImage(mimeType) && !MimeTypes.isVideo(mimeType)) {
// open with another app // open with another app
unawaited(androidAppService.open(uri, mimeType, forceChooser: true).then((success) { unawaited(appService.open(uri, mimeType, forceChooser: true).then((success) {
if (!success) { if (!success) {
// fallback to sharing, so that the file can be saved somewhere // fallback to sharing, so that the file can be saved somewhere
androidAppService.shareSingle(uri, mimeType).then((success) { appService.shareSingle(uri, mimeType).then((success) {
if (!success) showNoMatchingAppDialog(context); if (!success) showNoMatchingAppDialog(context);
}); });
} }

View file

@ -1,5 +1,5 @@
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/selection.dart'; import 'package:aves/model/selection.dart';

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/actions/events.dart'; import 'package:aves/model/actions/events.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/multipage.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/actions/slideshow_actions.dart'; import 'package:aves/model/actions/slideshow.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.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/common/identity/buttons/overlay_button.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/entry/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:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
import 'package:aves/widgets/viewer/overlay/video/controls.dart'; import 'package:aves/widgets/viewer/overlay/video/controls.dart';

View file

@ -1,7 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';

View file

@ -1,5 +1,5 @@
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/slideshow_actions.dart'; import 'package:aves/model/actions/slideshow.dart';
import 'package:aves/model/entry/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/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/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';

View file

@ -1,9 +1,9 @@
import 'package:aves/services/android_app_service.dart'; import 'package:aves/model/apps.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/services/app_service.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
class FakeAndroidAppService extends Fake implements AndroidAppService { class FakeAppService extends Fake implements AppService {
@override @override
Future<Set<Package>> getPackages() => SynchronousFuture({}); Future<Set<Package>> getPackages() => SynchronousFuture({});
} }

View file

@ -1,5 +1,5 @@
import 'package:aves/model/storage/volume.dart';
import 'package:aves/services/storage_service.dart'; import 'package:aves/services/storage_service.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';

View file

@ -13,7 +13,7 @@ import 'package:aves/model/metadata/catalog.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/media_store_source.dart'; import 'package:aves/model/source/media_store_source.dart';
import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/app_service.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/services/device_service.dart'; import 'package:aves/services/device_service.dart';
import 'package:aves/services/media/media_fetch_service.dart'; import 'package:aves/services/media/media_fetch_service.dart';
@ -59,7 +59,7 @@ void main() {
getIt.registerLazySingleton<AvesAvailability>(FakeAvesAvailability.new); getIt.registerLazySingleton<AvesAvailability>(FakeAvesAvailability.new);
getIt.registerLazySingleton<MetadataDb>(FakeMetadataDb.new); getIt.registerLazySingleton<MetadataDb>(FakeMetadataDb.new);
getIt.registerLazySingleton<AndroidAppService>(FakeAndroidAppService.new); getIt.registerLazySingleton<AppService>(FakeAppService.new);
getIt.registerLazySingleton<DeviceService>(FakeDeviceService.new); getIt.registerLazySingleton<DeviceService>(FakeDeviceService.new);
getIt.registerLazySingleton<MediaFetchService>(FakeMediaFetchService.new); getIt.registerLazySingleton<MediaFetchService>(FakeMediaFetchService.new);
getIt.registerLazySingleton<MediaStoreService>(FakeMediaStoreService.new); getIt.registerLazySingleton<MediaStoreService>(FakeMediaStoreService.new);