device capabilities, API <19 prep
This commit is contained in:
parent
c5a4048322
commit
089304da2d
18 changed files with 168 additions and 122 deletions
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -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<String>("label")
|
||||
val iconBytes = call.argument<ByteArray>("iconBytes")
|
||||
val filters = call.argument<List<String>>("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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -195,21 +195,31 @@ 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<String> {
|
||||
val grantedDirs = HashSet<String>()
|
||||
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
|
||||
}
|
||||
|
||||
// returns paths accessible to the app (granted by the user or by default)
|
||||
private fun getAccessibleDirs(context: Context): Set<String> {
|
||||
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)
|
||||
|
|
|
@ -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<bool> get canLocatePlaces => Future.wait<bool>([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<bool> get _isUseGoogleMapRenderingSupported => DeviceInfoPlugin().androidInfo.then((androidInfo) => (androidInfo.version.sdkInt ?? 0) >= 20);
|
||||
|
||||
@override
|
||||
Future<bool> get canUseGoogleMaps => Future.wait<bool>([_isUseGoogleMapRenderingSupported, hasPlayServices]).then((results) => results.every((result) => result));
|
||||
Future<bool> get canUseGoogleMaps async => device.canRenderGoogleMaps && await hasPlayServices;
|
||||
|
||||
@override
|
||||
Future<bool> get isNewVersionAvailable async {
|
||||
|
|
32
lib/model/device.dart
Normal file
32
lib/model/device.dart
Normal file
|
@ -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<void> 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;
|
||||
}
|
||||
}
|
|
@ -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<bool> shareSingle(String uri, String mimeType);
|
||||
|
||||
Future<bool> canPinToHomeScreen();
|
||||
|
||||
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? 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<bool> 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<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri}) async {
|
||||
Uint8List? iconBytes;
|
||||
|
@ -209,7 +187,7 @@ class PlatformAndroidAppService implements AndroidAppService {
|
|||
);
|
||||
}
|
||||
try {
|
||||
await platform.invokeMethod('pin', <String, dynamic>{
|
||||
await platform.invokeMethod('pinShortcut', <String, dynamic>{
|
||||
'label': label,
|
||||
'iconBytes': iconBytes,
|
||||
'filters': filters?.map((filter) => filter.toJson()).toList(),
|
||||
|
|
|
@ -2,6 +2,8 @@ import 'package:aves/services/common/services.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
|
||||
abstract class DeviceService {
|
||||
Future<Map<String, dynamic>> getCapabilities();
|
||||
|
||||
Future<String?> getDefaultTimeZone();
|
||||
|
||||
Future<int> getPerformanceClass();
|
||||
|
@ -10,6 +12,17 @@ abstract class DeviceService {
|
|||
class PlatformDeviceService implements DeviceService {
|
||||
static const platform = MethodChannel('deckers.thibault/aves/device');
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> getCapabilities() async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getCapabilities');
|
||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getDefaultTimeZone() async {
|
||||
try {
|
||||
|
|
|
@ -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<AvesApp> {
|
|||
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<AvesApp> {
|
|||
];
|
||||
}
|
||||
|
||||
Future<void> _initUserAgent() async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
AvesApp.userAgent = '${info.packageName}/${info.version}';
|
||||
}
|
||||
|
||||
void _onNewIntent(Map? intentData) {
|
||||
debugPrint('$runtimeType onNewIntent with intentData=$intentData');
|
||||
|
||||
|
|
|
@ -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<CollectionAppBar> with SingleTickerPr
|
|||
final List<StreamSubscription> _subscriptions = [];
|
||||
final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate();
|
||||
late AnimationController _browseToSelectAnimation;
|
||||
late Future<bool> _canAddShortcutsLoader;
|
||||
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
|
||||
final FocusNode _queryBarFocusNode = FocusNode();
|
||||
late final Listenable _queryFocusRequestNotifier;
|
||||
|
@ -69,7 +67,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
vsync: this,
|
||||
);
|
||||
_isSelectingNotifier.addListener(_onActivityChange);
|
||||
_canAddShortcutsLoader = androidAppService.canPinToHomeScreen();
|
||||
_registerWidget(widget);
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) => _onFilterChanged());
|
||||
}
|
||||
|
@ -104,10 +101,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||
return FutureBuilder<bool>(
|
||||
future: _canAddShortcutsLoader,
|
||||
builder: (context, snapshot) {
|
||||
final canAddShortcuts = snapshot.data ?? false;
|
||||
return Selector<Selection<AvesEntry>, Tuple2<bool, int>>(
|
||||
selector: (context, selection) => Tuple2(selection.isSelecting, selection.selectedItems.length),
|
||||
builder: (context, s, child) {
|
||||
|
@ -127,7 +120,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
actions: _buildActions(
|
||||
isSelecting: isSelecting,
|
||||
selectedItemCount: selectedItemCount,
|
||||
supportShortcuts: canAddShortcuts,
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(appBarBottomHeight),
|
||||
|
@ -156,8 +148,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double get appBarBottomHeight {
|
||||
|
@ -214,14 +204,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
List<Widget> _buildActions({
|
||||
required bool isSelecting,
|
||||
required int selectedItemCount,
|
||||
required bool supportShortcuts,
|
||||
}) {
|
||||
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||
bool isVisible(EntrySetAction action) => _actionDelegate.isVisible(
|
||||
action,
|
||||
appMode: appMode,
|
||||
isSelecting: isSelecting,
|
||||
supportShortcuts: supportShortcuts,
|
||||
sortFactor: collection.sortFactor,
|
||||
itemCount: collection.entryCount,
|
||||
selectedItemCount: selectedItemCount,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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<String, String> headers = {
|
||||
'User-Agent': AvesApp.userAgent,
|
||||
'User-Agent': device.userAgent,
|
||||
};
|
||||
|
||||
_NetworkTileProvider();
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue