refactor
This commit is contained in:
parent
ad3edf4458
commit
86b982d270
92 changed files with 573 additions and 435 deletions
|
@ -33,11 +33,23 @@ import java.util.regex.Pattern
|
|||
object 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"
|
||||
|
||||
// cf DocumentsContract.EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID
|
||||
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 TREE_URI_PATH_PATTERN = Pattern.compile("(.*?):(.*)")
|
||||
|
@ -545,7 +557,7 @@ object StorageUtils {
|
|||
uri ?: return false
|
||||
// a URI's authority is [userinfo@]host[:port]
|
||||
// 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 {
|
||||
|
@ -554,7 +566,7 @@ object StorageUtils {
|
|||
val path = uri.path
|
||||
path ?: return uri
|
||||
// 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"
|
||||
if (context.checkSelfPermission(Manifest.permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||
return MediaStore.setRequireOriginal(uri)
|
||||
|
@ -611,7 +623,7 @@ object StorageUtils {
|
|||
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
|
||||
// by the Media Store (via `.nomedia`), and therefore has no matching image/video content URI.
|
||||
private fun getMediaUriImageVideoUri(uri: Uri, mimeType: String): Uri? {
|
||||
|
|
|
@ -39,7 +39,7 @@ class AppIconImage extends ImageProvider<AppIconImageKey> {
|
|||
|
||||
Future<ui.Codec> _loadAsync(AppIconImageKey key, DecoderBufferCallback decode) async {
|
||||
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);
|
||||
return await decode(buffer);
|
||||
} catch (error) {
|
||||
|
|
|
@ -1 +1,7 @@
|
|||
enum MoveType { copy, move, export, toBin, fromBin }
|
||||
enum MoveType {
|
||||
copy,
|
||||
move,
|
||||
export,
|
||||
toBin,
|
||||
fromBin,
|
||||
}
|
||||
|
|
96
lib/model/app/support.dart
Normal file
96
lib/model/app/support.dart
Normal 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
78
lib/model/apps.dart
Normal 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}';
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/apps.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/vaults/vaults.dart';
|
||||
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/utils/android_file_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -121,7 +123,7 @@ class Covers {
|
|||
|
||||
String? effectiveAlbumPackage(String albumPath) {
|
||||
final filterPackage = of(AlbumFilter(albumPath, null))?.item2;
|
||||
return filterPackage ?? androidFileUtils.getAlbumAppPackageName(albumPath);
|
||||
return filterPackage ?? appInventory.getAlbumAppPackageName(albumPath);
|
||||
}
|
||||
|
||||
// import/export
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:aves/model/storage/relative_dir.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
final entryDirRepo = EntryDirRepo._private();
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
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/catalog.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/services/common/services.dart';
|
||||
import 'package:aves/theme/format.dart';
|
||||
import 'package:aves_utils/aves_utils.dart';
|
||||
import 'package:aves/utils/time_utils.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
import 'package:aves_utils/aves_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
|
@ -80,10 +78,6 @@ class AvesEntry with AvesEntryBase {
|
|||
this.durationMillis = durationMillis;
|
||||
}
|
||||
|
||||
bool get canDecode => !MimeTypes.undecodableImages.contains(mimeType);
|
||||
|
||||
bool get canHaveAlpha => MimeTypes.alphaImages.contains(mimeType);
|
||||
|
||||
AvesEntry copyWith({
|
||||
int? id,
|
||||
String? uri,
|
||||
|
@ -225,15 +219,6 @@ class AvesEntry with AvesEntryBase {
|
|||
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
|
||||
// so we use the one found during cataloguing if possible
|
||||
String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType;
|
||||
|
@ -323,18 +308,6 @@ class AvesEntry with AvesEntryBase {
|
|||
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
|
||||
// (0, 0) coordinates are considered invalid, as it is likely a default value
|
||||
bool get hasGps => (_catalogMetadata?.latitude ?? 0) != 0 || (_catalogMetadata?.longitude ?? 0) != 0;
|
||||
|
|
|
@ -1,65 +1,111 @@
|
|||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/app/support.dart';
|
||||
import 'package:aves/model/entry/entry.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/unicode.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/theme/text.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
|
||||
extension ExtraAvesEntryProps on AvesEntry {
|
||||
// type
|
||||
|
||||
String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*');
|
||||
|
||||
bool get canHaveAlpha => MimeTypes.canHaveAlpha(mimeType);
|
||||
|
||||
bool get isSvg => mimeType == MimeTypes.svg;
|
||||
|
||||
// guess whether this is a photo, according to file type (used as a hint to e.g. display megapixels)
|
||||
bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg, MimeTypes.tiff].contains(mimeType) || isRaw;
|
||||
|
||||
// Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported"
|
||||
// but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below,
|
||||
// and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested.
|
||||
bool get _supportedByBitmapRegionDecoder =>
|
||||
[
|
||||
MimeTypes.heic,
|
||||
MimeTypes.heif,
|
||||
MimeTypes.jpeg,
|
||||
MimeTypes.png,
|
||||
MimeTypes.webp,
|
||||
MimeTypes.arw,
|
||||
MimeTypes.cr2,
|
||||
MimeTypes.nef,
|
||||
MimeTypes.nrw,
|
||||
MimeTypes.orf,
|
||||
MimeTypes.pef,
|
||||
MimeTypes.raf,
|
||||
MimeTypes.rw2,
|
||||
MimeTypes.srw,
|
||||
].contains(mimeType) &&
|
||||
!isAnimated;
|
||||
|
||||
bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
|
||||
|
||||
bool get useTiles => supportTiling && (width > 4096 || height > 4096);
|
||||
|
||||
bool get isRaw => MimeTypes.rawImages.contains(mimeType);
|
||||
bool get isRaw => MimeTypes.isRaw(mimeType);
|
||||
|
||||
bool get isImage => MimeTypes.isImage(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 isGeotiff => catalogMetadata?.isGeotiff ?? 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);
|
||||
|
||||
|
@ -75,44 +121,17 @@ extension ExtraAvesEntryProps on AvesEntry {
|
|||
|
||||
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 {
|
||||
final ws = width;
|
||||
final hs = height;
|
||||
return isRotated ? '$hs${AText.resolutionSeparator}$ws' : '$ws${AText.resolutionSeparator}$hs';
|
||||
}
|
||||
bool get canEditXmp => AppSupport.canEditXmp(mimeType);
|
||||
|
||||
String get aspectRatioText {
|
||||
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();
|
||||
bool get canRemoveMetadata => AppSupport.canRemoveMetadata(mimeType);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:aves/model/entry/entry.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/utils/android_file_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:aves/model/covers.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/theme/colors.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:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/actions/entry_set_actions.dart';
|
||||
import 'package:aves/model/actions/entry.dart';
|
||||
import 'package:aves/model/actions/entry_set.dart';
|
||||
import 'package:aves/model/filters/recent.dart';
|
||||
import 'package:aves/model/naming_pattern.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
|
|
|
@ -3,8 +3,8 @@ import 'dart:convert';
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/app_flavor.dart';
|
||||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/actions/entry_set_actions.dart';
|
||||
import 'package:aves/model/actions/entry.dart';
|
||||
import 'package:aves/model/actions/entry_set.dart';
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
|
|
|
@ -2,6 +2,8 @@ import 'package:aves/model/entry/entry.dart';
|
|||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
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/services/common/services.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
|
|
|
@ -9,3 +9,14 @@ enum EntrySortFactor { date, name, rating, size }
|
|||
enum EntryGroupFactor { none, album, month, day }
|
||||
|
||||
enum TileLayout { mosaic, grid, list }
|
||||
|
||||
enum AlbumType {
|
||||
regular,
|
||||
vault,
|
||||
app,
|
||||
camera,
|
||||
download,
|
||||
screenRecordings,
|
||||
screenshots,
|
||||
videoCaptures,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/services/common/image_op_events.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
|
|
49
lib/model/storage/relative_dir.dart
Normal file
49
lib/model/storage/relative_dir.dart
Normal 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;
|
||||
}
|
||||
}
|
41
lib/model/storage/volume.dart
Normal file
41
lib/model/storage/volume.dart
Normal 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'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
|
||||
// TODO TLAD [codec] make it dynamic if it depends on OS/lib versions
|
||||
static const Set<String> undecodableImages = {art, cdr, crw, djvu, jpeg2000, jxl, pat, pcx, pnm, psdVnd, psdX, octetStream, zip};
|
||||
static bool canHaveAlpha(String mimeType) => MimeTypes.alphaImages.contains(mimeType);
|
||||
|
||||
static const Set<String> _knownOpaqueImages = {jpeg};
|
||||
|
||||
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 isRaw(String mimeType) => MimeTypes.rawImages.contains(mimeType);
|
||||
|
||||
static bool isImage(String mimeType) => mimeType.startsWith('image');
|
||||
|
||||
|
@ -147,56 +134,4 @@ class MimeTypes {
|
|||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import 'package:aves/model/apps.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/math_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
abstract class AndroidAppService {
|
||||
abstract class AppService {
|
||||
Future<Set<Package>> getPackages();
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
class PlatformAndroidAppService implements AndroidAppService {
|
||||
class PlatformAppService implements AppService {
|
||||
static const _platform = MethodChannel('deckers.thibault/aves/app');
|
||||
|
||||
static final _knownAppDirs = {
|
|
@ -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/settings/store/store.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/media/embedded_data_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 MetadataDb metadataDb = getIt<MetadataDb>();
|
||||
|
||||
final AndroidAppService androidAppService = getIt<AndroidAppService>();
|
||||
final AppService appService = getIt<AppService>();
|
||||
final DeviceService deviceService = getIt<DeviceService>();
|
||||
final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>();
|
||||
final MediaEditService mediaEditService = getIt<MediaEditService>();
|
||||
|
@ -51,7 +51,7 @@ void initPlatformServices() {
|
|||
getIt.registerLazySingleton<AvesAvailability>(LiveAvesAvailability.new);
|
||||
getIt.registerLazySingleton<MetadataDb>(SqfliteMetadataDb.new);
|
||||
|
||||
getIt.registerLazySingleton<AndroidAppService>(PlatformAndroidAppService.new);
|
||||
getIt.registerLazySingleton<AppService>(PlatformAppService.new);
|
||||
getIt.registerLazySingleton<DeviceService>(PlatformDeviceService.new);
|
||||
getIt.registerLazySingleton<EmbeddedDataService>(PlatformEmbeddedDataService.new);
|
||||
getIt.registerLazySingleton<MediaEditService>(PlatformMediaEditService.new);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/app/support.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/ref/mime_types.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
|
||||
return await completer.future;
|
||||
} on PlatformException catch (e, stack) {
|
||||
if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) {
|
||||
if (_isUnknownVisual(mimeType)) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
}
|
||||
|
@ -191,7 +192,7 @@ class PlatformMediaFetchService implements MediaFetchService {
|
|||
});
|
||||
if (result != null) return result as Uint8List;
|
||||
} on PlatformException catch (e, stack) {
|
||||
if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) {
|
||||
if (_isUnknownVisual(mimeType)) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
}
|
||||
|
@ -231,7 +232,7 @@ class PlatformMediaFetchService implements MediaFetchService {
|
|||
});
|
||||
if (result != null) return result as Uint8List;
|
||||
} on PlatformException catch (e, stack) {
|
||||
if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) {
|
||||
if (_isUnknownVisual(mimeType)) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
}
|
||||
|
@ -259,4 +260,47 @@ class PlatformMediaFetchService implements MediaFetchService {
|
|||
|
||||
@override
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
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/enums/enums.dart';
|
||||
import 'package:aves/model/metadata/enums/metadata_type.dart';
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
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/services.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:streams_channel/streams_channel.dart';
|
||||
|
||||
|
|
|
@ -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/services/common/services.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
final AndroidFileUtils androidFileUtils = AndroidFileUtils._private();
|
||||
|
||||
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';
|
||||
|
||||
late final String separator, vaultRoot, primaryStorage;
|
||||
late final String dcimPath, downloadPath, moviesPath, picturesPath, avesVideoCapturesPath;
|
||||
late final Set<String> videoCapturesPaths;
|
||||
Set<StorageVolume> storageVolumes = {};
|
||||
Set<Package> _packages = {};
|
||||
List<String> _potentialAppDirs = [];
|
||||
bool _initialized = false;
|
||||
|
||||
ValueNotifier<bool> areAppNamesReadyNotifier = ValueNotifier(false);
|
||||
|
||||
Iterable<Package> get _launcherPackages => _packages.where((package) => package.categoryLauncher);
|
||||
|
||||
AndroidFileUtils._private();
|
||||
|
||||
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 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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import 'package:aves/theme/durations.dart';
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/theme/styles.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/widgets/collection/collection_grid.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
|
@ -493,9 +493,9 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
void _monitorSettings() {
|
||||
void applyIsInstalledAppAccessAllowed() {
|
||||
if (settings.isInstalledAppAccessAllowed) {
|
||||
androidFileUtils.initAppNames();
|
||||
appInventory.initAppNames();
|
||||
} else {
|
||||
androidFileUtils.resetAppNames();
|
||||
appInventory.resetAppNames();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
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/filters/filters.dart';
|
||||
import 'package:aves/model/filters/query.dart';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
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/device.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_source.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/services.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
|
@ -264,7 +264,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
Future<void> _share(BuildContext context) async {
|
||||
final entries = _getTargetItems(context);
|
||||
try {
|
||||
if (!await androidAppService.shareEntries(entries)) {
|
||||
if (!await appService.shareEntries(entries)) {
|
||||
await showNoMatchingAppDialog(context);
|
||||
}
|
||||
} on TooManyItemsException catch (_) {
|
||||
|
@ -741,7 +741,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
final name = result.item2;
|
||||
if (name.isEmpty) return;
|
||||
|
||||
await androidAppService.pinToHomeScreen(name, coverEntry, filters: filters);
|
||||
await appService.pinToHomeScreen(name, coverEntry, filters: filters);
|
||||
if (!device.showPinShortcutFeedback) {
|
||||
showFeedback(context, context.l10n.genericSuccessFeedback);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/entry/entry.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/theme/icons.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
|
|
|
@ -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/filters.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
|
|
@ -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/rate_chooser.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/actions/share_actions.dart';
|
||||
import 'package:aves/model/actions/entry.dart';
|
||||
import 'package:aves/model/actions/share.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||
import 'package:aves/widgets/common/action_controls/quick_choosers/common/button.dart';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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/basic/popup/menu_row.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -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/tag.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:math';
|
|||
|
||||
import 'package:aves/model/actions/move_type.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/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/collection_utils.dart';
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:async';
|
|||
import 'dart:math';
|
||||
|
||||
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/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
|
|
|
@ -3,9 +3,9 @@ import 'package:aves/model/covers.dart';
|
|||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
import 'package:aves/model/source/enums/enums.dart';
|
||||
import 'package:aves/model/vaults/vaults.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/grid/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -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/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
|
|
|
@ -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/settings.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/common/basic/query_bar.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||
import 'package:aves/widgets/viewer/info/common.dart';
|
||||
|
@ -23,7 +23,7 @@ class _DebugAndroidAppSectionState extends State<DebugAndroidAppSection> with Au
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loader = androidAppService.getPackages();
|
||||
_loader = appService.getPackages();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/model/app/support.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/metadata/enums/enums.dart';
|
||||
import 'package:aves/model/metadata/enums/length_unit.dart';
|
||||
|
@ -205,7 +206,7 @@ class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
|||
valueListenable: _mimeTypeNotifier,
|
||||
builder: (context, mimeType, 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(
|
||||
value: _writeMetadata,
|
||||
onChanged: (v) => setState(() => _writeMetadata = v),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:aves/model/storage/volume.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
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/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/query_bar.dart';
|
||||
import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||
|
@ -34,7 +34,7 @@ class _AppPickPageState extends State<AppPickPage> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
_selectedValue = widget.initialValue;
|
||||
_loader = androidAppService.getPackages();
|
||||
_loader = appService.getPackages();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:aves/model/source/album.dart';
|
|||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums/enums.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/model/apps.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
|
@ -35,7 +36,7 @@ class AlbumListPage extends StatelessWidget {
|
|||
},
|
||||
builder: (context, s, child) {
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: androidFileUtils.areAppNamesReadyNotifier,
|
||||
valueListenable: appInventory.areAppNamesReadyNotifier,
|
||||
builder: (context, areAppNamesReady, child) {
|
||||
return StreamBuilder(
|
||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:io';
|
||||
|
||||
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/device.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/enums/enums.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/vaults.dart';
|
||||
import 'package:aves/services/common/image_op_events.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/services/media/enums.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/vault_aware.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
|
|
|
@ -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/filters.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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/entry/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
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/query.dart';
|
||||
import 'package:aves/model/selection.dart';
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'package:aves/model/vaults/vaults.dart';
|
|||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/theme/text.dart';
|
||||
import 'package:aves/model/apps.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.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) {
|
||||
// when we asynchronously fetch installed app names,
|
||||
// album filters themselves do not change, but decoration derived from it does
|
||||
chipKey = ValueKey(androidFileUtils.areAppNamesReadyNotifier.value);
|
||||
chipKey = ValueKey(appInventory.areAppNamesReadyNotifier.value);
|
||||
}
|
||||
return AvesFilterChip(
|
||||
key: chipKey,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:aves/model/source/section_keys.dart';
|
||||
import 'package:aves/model/storage/volume.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/filter_grids/common/enums.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
|
|
@ -17,6 +17,7 @@ import 'package:aves/services/common/services.dart';
|
|||
import 'package:aves/services/global_search.dart';
|
||||
import 'package:aves/services/intent_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/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||
|
@ -107,7 +108,7 @@ class _HomePageState extends State<HomePage> {
|
|||
|
||||
await androidFileUtils.init();
|
||||
if (!{actionScreenSaver, actionSetWallpaper}.contains(intentAction) && settings.isInstalledAppAccessAllowed) {
|
||||
unawaited(androidFileUtils.initAppNames());
|
||||
unawaited(appInventory.initAppNames());
|
||||
}
|
||||
|
||||
if (intentData.isNotEmpty) {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/actions/map_actions.dart';
|
||||
import 'package:aves/model/actions/map_cluster_actions.dart';
|
||||
import 'package:aves/model/actions/map.dart';
|
||||
import 'package:aves/model/actions/map_cluster.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/location.dart';
|
||||
import 'package:aves/model/filters/coordinate.dart';
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:aves/model/settings/settings.dart';
|
|||
import 'package:aves/model/source/album.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums/enums.dart';
|
||||
import 'package:aves/model/source/location/country.dart';
|
||||
import 'package:aves/model/source/location/place.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:aves/model/storage/relative_dir.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CrumbLine extends StatefulWidget {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import 'dart:io';
|
||||
|
||||
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/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:convert';
|
|||
import 'dart:math';
|
||||
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/source/collection_source.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
|
|
|
@ -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/widgets/common/basic/scaffold.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
|
|
|
@ -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/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart';
|
||||
|
|
|
@ -2,9 +2,9 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
|
||||
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/share_actions.dart';
|
||||
import 'package:aves/model/actions/share.dart';
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/favourites.dart';
|
||||
|
@ -188,7 +188,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
_addShortcut(context, targetEntry);
|
||||
break;
|
||||
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);
|
||||
});
|
||||
break;
|
||||
|
@ -214,7 +214,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
_move(context, targetEntry, moveType: MoveType.move);
|
||||
break;
|
||||
case EntryAction.share:
|
||||
androidAppService.shareEntries({targetEntry}).then((success) {
|
||||
appService.shareEntries({targetEntry}).then((success) {
|
||||
if (!success) showNoMatchingAppDialog(context);
|
||||
});
|
||||
break;
|
||||
|
@ -255,22 +255,22 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
}
|
||||
break;
|
||||
case EntryAction.edit:
|
||||
androidAppService.edit(targetEntry.uri, targetEntry.mimeType).then((success) {
|
||||
appService.edit(targetEntry.uri, targetEntry.mimeType).then((success) {
|
||||
if (!success) showNoMatchingAppDialog(context);
|
||||
});
|
||||
break;
|
||||
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);
|
||||
});
|
||||
break;
|
||||
case EntryAction.openMap:
|
||||
androidAppService.openMap(targetEntry.latLng!).then((success) {
|
||||
appService.openMap(targetEntry.latLng!).then((success) {
|
||||
if (!success) showNoMatchingAppDialog(context);
|
||||
});
|
||||
break;
|
||||
case EntryAction.setAs:
|
||||
androidAppService.setAs(targetEntry.uri, targetEntry.mimeType).then((success) {
|
||||
appService.setAs(targetEntry.uri, targetEntry.mimeType).then((success) {
|
||||
if (!success) showNoMatchingAppDialog(context);
|
||||
});
|
||||
break;
|
||||
|
@ -334,7 +334,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
final uri = fields['uri'] as String?;
|
||||
final mimeType = fields['mimeType'] as String?;
|
||||
if (uri != null && mimeType != null) {
|
||||
await androidAppService.shareSingle(uri, mimeType).then((success) {
|
||||
await appService.shareSingle(uri, mimeType).then((success) {
|
||||
if (!success) showNoMatchingAppDialog(context);
|
||||
});
|
||||
}
|
||||
|
@ -363,7 +363,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
final name = result.item2;
|
||||
if (name.isEmpty) return;
|
||||
|
||||
await androidAppService.pinToHomeScreen(name, targetEntry, uri: targetEntry.uri);
|
||||
await appService.pinToHomeScreen(name, targetEntry, uri: targetEntry.uri);
|
||||
if (!device.showPinShortcutFeedback) {
|
||||
showFeedback(context, context.l10n.genericSuccessFeedback);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
|
||||
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/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/info.dart';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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/extensions/location.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);
|
||||
break;
|
||||
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);
|
||||
});
|
||||
break;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/actions/entry.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ShowPreviousIntent extends Intent {
|
||||
|
|
|
@ -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/entry/entry.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
|
|
|
@ -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:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/favourites.dart';
|
||||
import 'package:aves/model/entry/extensions/location.dart';
|
||||
import 'package:aves/model/entry/extensions/images.dart';
|
||||
import 'package:aves/model/entry/extensions/location.dart';
|
||||
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
|
@ -130,7 +130,6 @@ class ViewerDebugPage extends StatelessWidget {
|
|||
'sizeBytes': '${entry.sizeBytes}',
|
||||
'isFavourite': '${entry.isFavourite}',
|
||||
'isSvg': '${entry.isSvg}',
|
||||
'isPhoto': '${entry.isPhoto}',
|
||||
'isVideo': '${entry.isVideo}',
|
||||
'isCatalogued': '${entry.isCatalogued}',
|
||||
'isAnimated': '${entry.isAnimated}',
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:math';
|
|||
import 'dart:ui';
|
||||
|
||||
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/extensions/catalog.dart';
|
||||
import 'package:aves/model/entry/extensions/location.dart';
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:async';
|
|||
import 'dart:math';
|
||||
|
||||
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/device.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/actions/entry.dart';
|
||||
import 'package:aves/model/apps.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/favourites.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/settings/settings.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/theme/colors.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/widgets/common/action_controls/quick_choosers/rate_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;
|
||||
|
||||
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' : ''}';
|
||||
|
||||
|
@ -291,7 +295,7 @@ class _BasicInfoState extends State<_BasicInfo> {
|
|||
});
|
||||
final isViewerMode = context.read<ValueNotifier<AppMode>>().value == AppMode.view;
|
||||
if (isViewerMode && settings.isInstalledAppAccessAllowed) {
|
||||
_appNameLoader = androidFileUtils.initAppNames();
|
||||
_appNameLoader = appInventory.initAppNames();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -349,7 +353,7 @@ class _BasicInfoState extends State<_BasicInfo> {
|
|||
InfoValueSpanBuilder _ownerHandler(String? ownerPackage) {
|
||||
if (ownerPackage == null) return (context, key, value) => [];
|
||||
|
||||
final appName = androidFileUtils.getCurrentAppName(ownerPackage) ?? ownerPackage;
|
||||
final appName = appInventory.getCurrentAppName(ownerPackage) ?? ownerPackage;
|
||||
return (context, key, value) => [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
|
|
|
@ -62,10 +62,10 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin {
|
|||
final uri = fields['uri']!;
|
||||
if (!MimeTypes.isImage(mimeType) && !MimeTypes.isVideo(mimeType)) {
|
||||
// open with another app
|
||||
unawaited(androidAppService.open(uri, mimeType, forceChooser: true).then((success) {
|
||||
unawaited(appService.open(uri, mimeType, forceChooser: true).then((success) {
|
||||
if (!success) {
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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/extensions/props.dart';
|
||||
import 'package:aves/model/selection.dart';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/multipage.dart';
|
||||
|
|
|
@ -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/widgets/common/identity/buttons/captioned_button.dart';
|
||||
import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
|
||||
|
|
|
@ -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/settings/enums/enums.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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/widgets/common/identity/buttons/overlay_button.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/video/controls.dart';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:math';
|
||||
|
||||
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/extensions/multipage.dart';
|
||||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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/filters/album.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
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/extensions/props.dart';
|
||||
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
||||
|
|
|
@ -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/extensions/multipage.dart';
|
||||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:aves/services/android_app_service.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/model/apps.dart';
|
||||
import 'package:aves/services/app_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:test/fake.dart';
|
||||
|
||||
class FakeAndroidAppService extends Fake implements AndroidAppService {
|
||||
class FakeAppService extends Fake implements AppService {
|
||||
@override
|
||||
Future<Set<Package>> getPackages() => SynchronousFuture({});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/storage/volume.dart';
|
||||
import 'package:aves/services/storage_service.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:test/fake.dart';
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import 'package:aves/model/metadata/catalog.dart';
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_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/device_service.dart';
|
||||
import 'package:aves/services/media/media_fetch_service.dart';
|
||||
|
@ -59,7 +59,7 @@ void main() {
|
|||
getIt.registerLazySingleton<AvesAvailability>(FakeAvesAvailability.new);
|
||||
getIt.registerLazySingleton<MetadataDb>(FakeMetadataDb.new);
|
||||
|
||||
getIt.registerLazySingleton<AndroidAppService>(FakeAndroidAppService.new);
|
||||
getIt.registerLazySingleton<AppService>(FakeAppService.new);
|
||||
getIt.registerLazySingleton<DeviceService>(FakeDeviceService.new);
|
||||
getIt.registerLazySingleton<MediaFetchService>(FakeMediaFetchService.new);
|
||||
getIt.registerLazySingleton<MediaStoreService>(FakeMediaStoreService.new);
|
||||
|
|
Loading…
Reference in a new issue