diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt index d4cfcb806..b05c850e3 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt @@ -44,7 +44,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() { val messenger = backgroundFlutterEngine!!.dartExecutor.binaryMessenger // channels for analysis - MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler()) + MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this)) MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt index 7e5871641..d13ddcaaa 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -59,7 +59,7 @@ class MainActivity : FlutterActivity() { MethodChannel(messenger, AnalysisHandler.CHANNEL).setMethodCallHandler(analysisHandler) MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this)) MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this)) - MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler()) + MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this)) MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this)) MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this)) MethodChannel(messenger, GlobalSearchHandler.CHANNEL).setMethodCallHandler(GlobalSearchHandler(this)) @@ -163,11 +163,13 @@ class MainActivity : FlutterActivity() { return } - // save access permissions across reboots - val takeFlags = (data.flags - and (Intent.FLAG_GRANT_READ_URI_PERMISSION - or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) - contentResolver.takePersistableUriPermission(treeUri, takeFlags) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + // save access permissions across reboots + val takeFlags = (data.flags + and (Intent.FLAG_GRANT_READ_URI_PERMISSION + or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) + contentResolver.takePersistableUriPermission(treeUri, takeFlags) + } // resume pending action onStorageAccessResult(requestCode, treeUri) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt index 8c7f0b9ad..6472a3149 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -51,8 +51,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { "openMap" -> safe(call, result, ::openMap) "setAs" -> safe(call, result, ::setAs) "share" -> safe(call, result, ::share) - "canPin" -> safe(call, result, ::canPin) - "pin" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::pin) } + "pinShortcut" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::pinShortcut) } else -> result.notImplemented() } } @@ -323,13 +322,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { // shortcuts - private fun isPinSupported() = ShortcutManagerCompat.isRequestPinShortcutSupported(context) - - private fun canPin(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { - result.success(isPinSupported()) - } - - private fun pin(call: MethodCall, result: MethodChannel.Result) { + private fun pinShortcut(call: MethodCall, result: MethodChannel.Result) { val label = call.argument("label") val iconBytes = call.argument("iconBytes") val filters = call.argument>("filters") @@ -339,7 +332,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { return } - if (!isPinSupported()) { + if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context)) { result.error("pin-unsupported", "failed because the launcher does not support pinning shortcuts", null) return } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt index 4240743d0..a6160eaaf 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt @@ -1,21 +1,36 @@ package deckers.thibault.aves.channel.calls +import android.content.Context import android.os.Build +import androidx.core.content.pm.ShortcutManagerCompat import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import java.util.* -class DeviceHandler : MethodCallHandler { +class DeviceHandler(private val context: Context) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { + "getCapabilities" -> safe(call, result, ::getCapabilities) "getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone) "getPerformanceClass" -> safe(call, result, ::getPerformanceClass) else -> result.notImplemented() } } + private fun getCapabilities(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + result.success(hashMapOf( + "canGrantDirectoryAccess" to (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP), + "canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context), + "canPrint" to (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT), + // as of google_maps_flutter v2.1.1, minSDK is 20 because of default PlatformView usage, + // but using hybrid composition would make it usable on API 19 too, + // cf https://github.com/flutter/flutter/issues/23728 + "canRenderGoogleMaps" to (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH), + )) + } + private fun getDefaultTimeZone(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { result.success(TimeZone.getDefault().id) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt index 2d1f7d4b3..2395c6170 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt @@ -73,6 +73,7 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.text.ParseException import java.util.* @@ -189,7 +190,15 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { val kv = pair as KeyValuePair val key = kv.key // `PNG-iTXt` uses UTF-8, contrary to `PNG-tEXt` and `PNG-zTXt` using Latin-1 / ISO-8859-1 - val charset = if (baseDirName == PNG_ITXT_DIR_NAME) StandardCharsets.UTF_8 else kv.value.charset + val charset = if (baseDirName == PNG_ITXT_DIR_NAME) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + StandardCharsets.UTF_8 + } else { + Charset.forName("UTF-8") + } + } else { + kv.value.charset + } val valueString = String(kv.value.bytes, charset) val dirs = extractPngProfile(key, valueString) if (dirs?.any() == true) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt index 9df3d5169..35c564e73 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt @@ -91,6 +91,12 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? } private fun createFile() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + // TODO TLAD [<=API18] create file + error("createFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null) + return + } + val name = args["name"] as String? val mimeType = args["mimeType"] as String? val bytes = args["bytes"] as ByteArray? @@ -128,6 +134,12 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? private fun openFile() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + // TODO TLAD [<=API18] open file + error("openFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null) + return + } + val mimeType = args["mimeType"] as String? if (mimeType == null) { error("openFile-args", "failed because of missing arguments", null) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt index 588927a9a..3cfa5f1bd 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt @@ -54,9 +54,11 @@ object MultiPage { // do not use `MediaFormat.KEY_TRACK_ID` as it is actually not unique between tracks // e.g. there could be both a video track and an image track with KEY_TRACK_ID == 1 - format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { track[KEY_IS_DEFAULT] = it != 0 } format.getSafeInt(MediaFormat.KEY_WIDTH) { track[KEY_WIDTH] = it } format.getSafeInt(MediaFormat.KEY_HEIGHT) { track[KEY_HEIGHT] = it } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { track[KEY_IS_DEFAULT] = it != 0 } + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { format.getSafeInt(MediaFormat.KEY_ROTATION) { track[KEY_ROTATION_DEGREES] = it } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt index 32c40d1da..7a06458ca 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt @@ -195,12 +195,14 @@ object PermissionManager { } ?: false } - // returns paths matching URIs granted by the user + // returns paths matching directory URIs granted by the user fun getGrantedDirs(context: Context): Set { val grantedDirs = HashSet() - for (uriPermission in context.contentResolver.persistedUriPermissions) { - val dirPath = StorageUtils.convertTreeUriToDirPath(context, uriPermission.uri) - dirPath?.let { grantedDirs.add(it) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + for (uriPermission in context.contentResolver.persistedUriPermissions) { + val dirPath = StorageUtils.convertTreeUriToDirPath(context, uriPermission.uri) + dirPath?.let { grantedDirs.add(it) } + } } return grantedDirs } @@ -208,8 +210,16 @@ object PermissionManager { // returns paths accessible to the app (granted by the user or by default) private fun getAccessibleDirs(context: Context): Set { val accessibleDirs = HashSet(getGrantedDirs(context)) - // from Android R, we no longer have access permission by default on primary volume - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + + // until API 18 / Android 4.3 / Jelly Bean MR2, removable storage is accessible by default like primary storage + // from API 19 / Android 4.4 / KitKat, removable storage requires access permission, at the file level + // from API 21 / Android 5.0 / Lollipop, removable storage requires access permission, but directory access grant is possible + // from API 30 / Android 11 / R, any storage requires access permission + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) { + accessibleDirs.addAll(StorageUtils.getVolumePaths(context)) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + && Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q + ) { accessibleDirs.add(StorageUtils.getPrimaryVolumePath(context)) } return accessibleDirs @@ -234,6 +244,7 @@ object PermissionManager { } } + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun releaseUriPermission(context: Context, it: Uri) { val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION context.contentResolver.releasePersistableUriPermission(it, flags) diff --git a/lib/model/availability.dart b/lib/model/availability.dart index 2ba3c216f..ebb7215e1 100644 --- a/lib/model/availability.dart +++ b/lib/model/availability.dart @@ -1,7 +1,7 @@ +import 'package:aves/model/device.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:github/github.dart'; @@ -62,12 +62,8 @@ class LiveAvesAvailability implements AvesAvailability { @override Future get canLocatePlaces => Future.wait([isConnected, hasPlayServices]).then((results) => results.every((result) => result)); - // as of google_maps_flutter v2.1.1, minSDK is 20 because of default PlatformView usage, - // but using hybrid composition would make it usable on 19 too, cf https://github.com/flutter/flutter/issues/23728 - Future get _isUseGoogleMapRenderingSupported => DeviceInfoPlugin().androidInfo.then((androidInfo) => (androidInfo.version.sdkInt ?? 0) >= 20); - @override - Future get canUseGoogleMaps => Future.wait([_isUseGoogleMapRenderingSupported, hasPlayServices]).then((results) => results.every((result) => result)); + Future get canUseGoogleMaps async => device.canRenderGoogleMaps && await hasPlayServices; @override Future get isNewVersionAvailable async { diff --git a/lib/model/device.dart b/lib/model/device.dart new file mode 100644 index 000000000..0a3cf3ea4 --- /dev/null +++ b/lib/model/device.dart @@ -0,0 +1,32 @@ +import 'package:aves/services/common/services.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +final Device device = Device._private(); + +class Device { + late final String _userAgent; + late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderGoogleMaps; + + String get userAgent => _userAgent; + + bool get canGrantDirectoryAccess => _canGrantDirectoryAccess; + + bool get canPinShortcut => _canPinShortcut; + + bool get canPrint => _canPrint; + + bool get canRenderGoogleMaps => _canRenderGoogleMaps; + + Device._private(); + + Future init() async { + final packageInfo = await PackageInfo.fromPlatform(); + _userAgent = '${packageInfo.packageName}/${packageInfo.version}'; + + final capabilities = await deviceService.getCapabilities(); + _canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false; + _canPinShortcut = capabilities['canPinShortcut'] ?? false; + _canPrint = capabilities['canPrint'] ?? false; + _canRenderGoogleMaps = capabilities['canRenderGoogleMaps'] ?? false; + } +} diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index a4685b1de..b78690476 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.dart @@ -6,7 +6,6 @@ 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/foundation.dart'; import 'package:flutter/services.dart'; import 'package:latlong2/latlong.dart'; @@ -29,8 +28,6 @@ abstract class AndroidAppService { Future shareSingle(String uri, String mimeType); - Future canPinToHomeScreen(); - Future pinToHomeScreen(String label, AvesEntry? coverEntry, {Set? filters, String? uri}); } @@ -174,25 +171,6 @@ class PlatformAndroidAppService implements AndroidAppService { // app shortcuts - // this ability will not change over the lifetime of the app - bool? _canPin; - - @override - Future canPinToHomeScreen() async { - if (_canPin != null) return SynchronousFuture(_canPin!); - - try { - final result = await platform.invokeMethod('canPin'); - if (result != null) { - _canPin = result; - return result; - } - } on PlatformException catch (e, stack) { - await reportService.recordError(e, stack); - } - return false; - } - @override Future pinToHomeScreen(String label, AvesEntry? coverEntry, {Set? filters, String? uri}) async { Uint8List? iconBytes; @@ -209,7 +187,7 @@ class PlatformAndroidAppService implements AndroidAppService { ); } try { - await platform.invokeMethod('pin', { + await platform.invokeMethod('pinShortcut', { 'label': label, 'iconBytes': iconBytes, 'filters': filters?.map((filter) => filter.toJson()).toList(), diff --git a/lib/services/device_service.dart b/lib/services/device_service.dart index cacbc5882..1f08e9baf 100644 --- a/lib/services/device_service.dart +++ b/lib/services/device_service.dart @@ -2,6 +2,8 @@ import 'package:aves/services/common/services.dart'; import 'package:flutter/services.dart'; abstract class DeviceService { + Future> getCapabilities(); + Future getDefaultTimeZone(); Future getPerformanceClass(); @@ -10,6 +12,17 @@ abstract class DeviceService { class PlatformDeviceService implements DeviceService { static const platform = MethodChannel('deckers.thibault/aves/device'); + @override + Future> getCapabilities() async { + try { + final result = await platform.invokeMethod('getCapabilities'); + if (result != null) return (result as Map).cast(); + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return {}; + } + @override Future getDefaultTimeZone() async { try { diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index a5a746ea4..607211839 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:aves/app_flavor.dart'; import 'package:aves/app_mode.dart'; +import 'package:aves/model/device.dart'; import 'package:aves/model/settings/accessibility_animations.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; @@ -28,15 +29,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:overlay_support/overlay_support.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class AvesApp extends StatefulWidget { final AppFlavor flavor; - static String userAgent = ''; - const AvesApp({ Key? key, required this.flavor, @@ -165,7 +163,7 @@ class _AvesAppState extends State { isRotationLocked: await windowService.isRotationLocked(), areAnimationsRemoved: await AccessibilityService.areAnimationsRemoved(), ); - unawaited(_initUserAgent()); + await device.init(); FijkLog.setLevel(FijkLogLevel.Warn); // keep screen on @@ -210,11 +208,6 @@ class _AvesAppState extends State { ]; } - Future _initUserAgent() async { - final info = await PackageInfo.fromPlatform(); - AvesApp.userAgent = '${info.packageName}/${info.version}'; - } - void _onNewIntent(Map? intentData) { debugPrint('$runtimeType onNewIntent with intentData=$intentData'); diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index baeed3e2d..1df527bc0 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -10,7 +10,6 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; -import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; @@ -46,7 +45,6 @@ class _CollectionAppBarState extends State with SingleTickerPr final List _subscriptions = []; final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate(); late AnimationController _browseToSelectAnimation; - late Future _canAddShortcutsLoader; final ValueNotifier _isSelectingNotifier = ValueNotifier(false); final FocusNode _queryBarFocusNode = FocusNode(); late final Listenable _queryFocusRequestNotifier; @@ -69,7 +67,6 @@ class _CollectionAppBarState extends State with SingleTickerPr vsync: this, ); _isSelectingNotifier.addListener(_onActivityChange); - _canAddShortcutsLoader = androidAppService.canPinToHomeScreen(); _registerWidget(widget); WidgetsBinding.instance!.addPostFrameCallback((_) => _onFilterChanged()); } @@ -104,53 +101,46 @@ class _CollectionAppBarState extends State with SingleTickerPr @override Widget build(BuildContext context) { final appMode = context.watch>().value; - return FutureBuilder( - future: _canAddShortcutsLoader, - builder: (context, snapshot) { - final canAddShortcuts = snapshot.data ?? false; - return Selector, Tuple2>( - selector: (context, selection) => Tuple2(selection.isSelecting, selection.selectedItems.length), - builder: (context, s, child) { - final isSelecting = s.item1; - final selectedItemCount = s.item2; - _isSelectingNotifier.value = isSelecting; - return AnimatedBuilder( - animation: collection.filterChangeNotifier, - builder: (context, child) { - final removableFilters = appMode != AppMode.pickInternal; - return Selector( - selector: (context, query) => query.enabled, - builder: (context, queryEnabled, child) { - return SliverAppBar( - leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null, - title: _buildAppBarTitle(isSelecting), - actions: _buildActions( - isSelecting: isSelecting, - selectedItemCount: selectedItemCount, - supportShortcuts: canAddShortcuts, - ), - bottom: PreferredSize( - preferredSize: Size.fromHeight(appBarBottomHeight), - child: Column( - children: [ - if (showFilterBar) - FilterBar( - filters: collection.filters.where((v) => !(v is QueryFilter && v.live)).toSet(), - removable: removableFilters, - onTap: removableFilters ? collection.removeFilter : null, - ), - if (queryEnabled) - EntryQueryBar( - queryNotifier: context.select>((query) => query.queryNotifier), - focusNode: _queryBarFocusNode, - ) - ], - ), - ), - titleSpacing: 0, - floating: true, - ); - }, + return Selector, Tuple2>( + selector: (context, selection) => Tuple2(selection.isSelecting, selection.selectedItems.length), + builder: (context, s, child) { + final isSelecting = s.item1; + final selectedItemCount = s.item2; + _isSelectingNotifier.value = isSelecting; + return AnimatedBuilder( + animation: collection.filterChangeNotifier, + builder: (context, child) { + final removableFilters = appMode != AppMode.pickInternal; + return Selector( + selector: (context, query) => query.enabled, + builder: (context, queryEnabled, child) { + return SliverAppBar( + leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null, + title: _buildAppBarTitle(isSelecting), + actions: _buildActions( + isSelecting: isSelecting, + selectedItemCount: selectedItemCount, + ), + bottom: PreferredSize( + preferredSize: Size.fromHeight(appBarBottomHeight), + child: Column( + children: [ + if (showFilterBar) + FilterBar( + filters: collection.filters.where((v) => !(v is QueryFilter && v.live)).toSet(), + removable: removableFilters, + onTap: removableFilters ? collection.removeFilter : null, + ), + if (queryEnabled) + EntryQueryBar( + queryNotifier: context.select>((query) => query.queryNotifier), + focusNode: _queryBarFocusNode, + ) + ], + ), + ), + titleSpacing: 0, + floating: true, ); }, ); @@ -214,14 +204,12 @@ class _CollectionAppBarState extends State with SingleTickerPr List _buildActions({ required bool isSelecting, required int selectedItemCount, - required bool supportShortcuts, }) { final appMode = context.watch>().value; bool isVisible(EntrySetAction action) => _actionDelegate.isVisible( action, appMode: appMode, isSelecting: isSelecting, - supportShortcuts: supportShortcuts, sortFactor: collection.sortFactor, itemCount: collection.entryCount, selectedItemCount: selectedItemCount, diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 3a8d7b6e2..63eaca49d 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/actions/move_type.dart'; +import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_xmp_iptc.dart'; import 'package:aves/model/filters/album.dart'; @@ -44,7 +45,6 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa EntrySetAction action, { required AppMode appMode, required bool isSelecting, - required bool supportShortcuts, required EntrySortFactor sortFactor, required int itemCount, required int selectedItemCount, @@ -67,7 +67,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa case EntrySetAction.toggleTitleSearch: return !isSelecting; case EntrySetAction.addShortcut: - return appMode == AppMode.main && !isSelecting && supportShortcuts; + return appMode == AppMode.main && !isSelecting && device.canPinShortcut; // browsing or selecting case EntrySetAction.map: case EntrySetAction.stats: diff --git a/lib/widgets/common/map/leaflet/tile_layers.dart b/lib/widgets/common/map/leaflet/tile_layers.dart index d815e1372..759502bcb 100644 --- a/lib/widgets/common/map/leaflet/tile_layers.dart +++ b/lib/widgets/common/map/leaflet/tile_layers.dart @@ -1,4 +1,4 @@ -import 'package:aves/widgets/aves_app.dart'; +import 'package:aves/model/device.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:provider/provider.dart'; @@ -53,7 +53,7 @@ class StamenWatercolorLayer extends StatelessWidget { class _NetworkTileProvider extends NetworkTileProvider { final Map headers = { - 'User-Agent': AvesApp.userAgent, + 'User-Agent': device.userAgent, }; _NetworkTileProvider(); diff --git a/lib/widgets/settings/privacy/privacy.dart b/lib/widgets/settings/privacy/privacy.dart index ec570ca87..d00919fc2 100644 --- a/lib/widgets/settings/privacy/privacy.dart +++ b/lib/widgets/settings/privacy/privacy.dart @@ -1,4 +1,5 @@ import 'package:aves/app_flavor.dart'; +import 'package:aves/model/device.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/color_utils.dart'; @@ -63,7 +64,7 @@ class PrivacySection extends StatelessWidget { ), ), const HiddenItemsTile(), - const StorageAccessTile(), + if (device.canGrantDirectoryAccess) const StorageAccessTile(), ], ); } diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index 427ed1a7b..601ae8986 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -1,4 +1,5 @@ import 'package:aves/model/actions/entry_actions.dart'; +import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/settings/settings.dart'; @@ -79,7 +80,7 @@ class ViewerTopOverlay extends StatelessWidget { return targetEntry.canRotateAndFlip; case EntryAction.export: case EntryAction.print: - return !targetEntry.isVideo; + return !targetEntry.isVideo && device.canPrint; case EntryAction.openMap: return targetEntry.hasGps; case EntryAction.viewSource: