#174 screen saver filter pick

This commit is contained in:
Thibault Deckers 2022-06-26 16:49:59 +09:00
parent 59a8dbe311
commit c418a9c144
46 changed files with 490 additions and 277 deletions

View file

@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
- Search: `on this day` filter
- Stats: histogram and date filters
- Screen saver
### Changed

View file

@ -84,7 +84,7 @@ open class MainActivity : FlutterActivity() {
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) }
// - need Activity
StreamsChannel(messenger, ImageOpStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageOpStreamHandler(this, args) }
StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL).setStreamHandlerFactory { args -> StorageAccessStreamHandler(this, args) }
StreamsChannel(messenger, ActivityResultStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ActivityResultStreamHandler(this, args) }
// change monitoring: platform -> dart
mediaStoreChangeStreamHandler = MediaStoreChangeStreamHandler(this).apply {
@ -99,15 +99,16 @@ open class MainActivity : FlutterActivity() {
intentStreamHandler = IntentStreamHandler().apply {
EventChannel(messenger, IntentStreamHandler.CHANNEL).setStreamHandler(this)
}
// detail fetch: dart -> platform
// intent detail & result: dart -> platform
intentDataMap = extractIntentData(intent)
MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result ->
MethodChannel(messenger, INTENT_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"getIntentData" -> {
result.success(intentDataMap)
intentDataMap.clear()
}
"pick" -> pick(call)
"submitPickedItems" -> submitPickedItems(call)
"submitPickedCollectionFilters" -> submitPickedCollectionFilters(call)
}
}
@ -162,27 +163,33 @@ open class MainActivity : FlutterActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(data, resultCode, requestCode)
DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(requestCode, resultCode, data)
DELETE_SINGLE_PERMISSION_REQUEST,
MEDIA_WRITE_BULK_PERMISSION_REQUEST -> onScopedStoragePermissionResult(resultCode)
CREATE_FILE_REQUEST,
OPEN_FILE_REQUEST -> onStorageAccessResult(requestCode, data?.data)
PICK_COLLECTION_FILTERS_REQUEST -> onCollectionFiltersPickResult(resultCode, data)
}
}
@SuppressLint("WrongConstant", "ObsoleteSdkInt")
private fun onDocumentTreeAccessResult(data: Intent?, resultCode: Int, requestCode: Int) {
val treeUri = data?.data
private fun onCollectionFiltersPickResult(resultCode: Int, intent: Intent?) {
val filters = if (resultCode == RESULT_OK) extractFiltersFromIntent(intent) else null
pendingCollectionFilterPickHandler?.let { it(filters) }
}
private fun onDocumentTreeAccessResult(requestCode: Int, resultCode: Int, intent: Intent?) {
val treeUri = intent?.data
if (resultCode != RESULT_OK || treeUri == null) {
onStorageAccessResult(requestCode, null)
return
}
@SuppressLint("WrongConstant", "ObsoleteSdkInt")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
val canPersist = (data.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0
val canPersist = (intent.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0
if (canPersist) {
// save access permissions across reboots
val takeFlags = (data.flags
val takeFlags = (intent.flags
and (Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
try {
@ -206,15 +213,8 @@ open class MainActivity : FlutterActivity() {
open fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
when (intent?.action) {
Intent.ACTION_MAIN -> {
intent.getStringExtra(SHORTCUT_KEY_PAGE)?.let { page ->
var filters = intent.getStringArrayExtra(SHORTCUT_KEY_FILTERS_ARRAY)?.toList()
if (filters == null) {
// fallback for shortcuts created on API < 26
val filterString = intent.getStringExtra(SHORTCUT_KEY_FILTERS_STRING)
if (filterString != null) {
filters = filterString.split(EXTRA_STRING_ARRAY_SEPARATOR)
}
}
intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page ->
val filters = extractFiltersFromIntent(intent)
return hashMapOf(
INTENT_DATA_KEY_PAGE to page,
INTENT_DATA_KEY_FILTERS to filters,
@ -234,7 +234,7 @@ open class MainActivity : FlutterActivity() {
}
Intent.ACTION_GET_CONTENT, Intent.ACTION_PICK -> {
return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK,
INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK_ITEMS,
INTENT_DATA_KEY_MIME_TYPE to intent.type,
INTENT_DATA_KEY_ALLOW_MULTIPLE to (intent.extras?.getBoolean(Intent.EXTRA_ALLOW_MULTIPLE) ?: false),
)
@ -250,6 +250,13 @@ open class MainActivity : FlutterActivity() {
INTENT_DATA_KEY_QUERY to intent.getStringExtra(SearchManager.QUERY),
)
}
INTENT_ACTION_PICK_COLLECTION_FILTERS -> {
val initialFilters = extractFiltersFromIntent(intent)
return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK_COLLECTION_FILTERS,
INTENT_DATA_KEY_FILTERS to initialFilters
)
}
Intent.ACTION_RUN -> {
// flutter run
}
@ -260,7 +267,22 @@ open class MainActivity : FlutterActivity() {
return HashMap()
}
private fun pick(call: MethodCall) {
private fun extractFiltersFromIntent(intent: Intent?): List<String>? {
intent ?: return null
val filters = intent.getStringArrayExtra(EXTRA_KEY_FILTERS_ARRAY)?.toList()
if (filters != null) return filters
// fallback for shortcuts created on API < 26
val filterString = intent.getStringExtra(EXTRA_KEY_FILTERS_STRING)
if (filterString != null) {
return filterString.split(EXTRA_STRING_ARRAY_SEPARATOR)
}
return null
}
private fun submitPickedItems(call: MethodCall) {
val pickedUris = call.argument<List<String>>("uris")
if (pickedUris != null && pickedUris.isNotEmpty()) {
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(context, Uri.parse(uriString)) }
@ -284,6 +306,19 @@ open class MainActivity : FlutterActivity() {
finish()
}
private fun submitPickedCollectionFilters(call: MethodCall) {
val filters = call.argument<List<String>>("filters")
if (filters != null) {
val intent = Intent()
.putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray())
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
setResult(RESULT_OK, intent)
} else {
setResult(RESULT_CANCELED)
}
finish()
}
@RequiresApi(Build.VERSION_CODES.N_MR1)
private fun setupShortcuts() {
// do not use 'route' as extra key, as the Flutter framework acts on it
@ -297,7 +332,7 @@ open class MainActivity : FlutterActivity() {
.setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_search else R.drawable.ic_shortcut_search))
.setIntent(
Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
.putExtra(SHORTCUT_KEY_PAGE, "/search")
.putExtra(EXTRA_KEY_PAGE, "/search")
)
.build()
@ -306,7 +341,7 @@ open class MainActivity : FlutterActivity() {
.setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_movie else R.drawable.ic_shortcut_movie))
.setIntent(
Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
.putExtra(SHORTCUT_KEY_PAGE, "/collection")
.putExtra(EXTRA_KEY_PAGE, "/collection")
.putExtra("filters", arrayOf("{\"type\":\"mime\",\"mime\":\"video/*\"}"))
)
.build()
@ -320,7 +355,7 @@ open class MainActivity : FlutterActivity() {
companion object {
private val LOG_TAG = LogUtils.createTag<MainActivity>()
const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer"
const val INTENT_CHANNEL = "deckers.thibault/aves/intent"
const val EXTRA_STRING_ARRAY_SEPARATOR = "###"
const val DOCUMENT_TREE_ACCESS_REQUEST = 1
const val OPEN_FROM_ANALYSIS_SERVICE = 2
@ -328,6 +363,7 @@ open class MainActivity : FlutterActivity() {
const val OPEN_FILE_REQUEST = 4
const val DELETE_SINGLE_PERMISSION_REQUEST = 5
const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 6
const val PICK_COLLECTION_FILTERS_REQUEST = 7
const val INTENT_DATA_KEY_ACTION = "action"
const val INTENT_DATA_KEY_FILTERS = "filters"
@ -337,22 +373,25 @@ open class MainActivity : FlutterActivity() {
const val INTENT_DATA_KEY_URI = "uri"
const val INTENT_DATA_KEY_QUERY = "query"
const val INTENT_ACTION_PICK = "pick"
const val INTENT_ACTION_PICK_ITEMS = "pick_items"
const val INTENT_ACTION_PICK_COLLECTION_FILTERS = "pick_collection_filters"
const val INTENT_ACTION_SCREEN_SAVER = "screen_saver"
const val INTENT_ACTION_SCREEN_SAVER_SETTINGS = "screen_saver_settings"
const val INTENT_ACTION_SEARCH = "search"
const val INTENT_ACTION_SET_WALLPAPER = "set_wallpaper"
const val INTENT_ACTION_VIEW = "view"
const val SHORTCUT_KEY_PAGE = "page"
const val SHORTCUT_KEY_FILTERS_ARRAY = "filters"
const val SHORTCUT_KEY_FILTERS_STRING = "filtersString"
const val EXTRA_KEY_PAGE = "page"
const val EXTRA_KEY_FILTERS_ARRAY = "filters"
const val EXTRA_KEY_FILTERS_STRING = "filtersString"
// request code to pending runnable
val pendingStorageAccessResultHandlers = ConcurrentHashMap<Int, PendingStorageAccessResultHandler>()
var pendingScopedStoragePermissionCompleter: CompletableFuture<Boolean>? = null
var pendingCollectionFilterPickHandler: ((filters: List<String>?) -> Unit)? = null
private fun onStorageAccessResult(requestCode: Int, uri: Uri?) {
Log.i(LOG_TAG, "onStorageAccessResult with requestCode=$requestCode, uri=$uri")
val handler = pendingStorageAccessResultHandlers.remove(requestCode) ?: return

View file

@ -116,7 +116,7 @@ class ScreenSaverService : DreamService() {
// intent handling
// detail fetch: dart -> platform
MethodChannel(messenger, WallpaperActivity.VIEWER_CHANNEL).setMethodCallHandler { call, result ->
MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"getIntentData" -> {
result.success(intentDataMap)

View file

@ -49,7 +49,7 @@ class WallpaperActivity : FlutterActivity() {
// intent handling
// detail fetch: dart -> platform
intentDataMap = extractIntentData(intent)
MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result ->
MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"getIntentData" -> {
result.success(intentDataMap)
@ -73,16 +73,6 @@ class WallpaperActivity : FlutterActivity() {
}
}
override fun onStop() {
Log.i(LOG_TAG, "onStop")
super.onStop()
}
override fun onDestroy() {
Log.i(LOG_TAG, "onDestroy")
super.onDestroy()
}
private fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
when (intent?.action) {
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
@ -108,6 +98,5 @@ class WallpaperActivity : FlutterActivity() {
companion object {
private val LOG_TAG = LogUtils.createTag<WallpaperActivity>()
const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer"
}
}

View file

@ -20,9 +20,9 @@ import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.MainActivity.Companion.EXTRA_STRING_ARRAY_SEPARATOR
import deckers.thibault.aves.MainActivity.Companion.SHORTCUT_KEY_FILTERS_ARRAY
import deckers.thibault.aves.MainActivity.Companion.SHORTCUT_KEY_FILTERS_STRING
import deckers.thibault.aves.MainActivity.Companion.SHORTCUT_KEY_PAGE
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_ARRAY
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE
import deckers.thibault.aves.R
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
@ -407,11 +407,11 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val intent = when {
uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java)
filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(SHORTCUT_KEY_PAGE, "/collection")
.putExtra(SHORTCUT_KEY_FILTERS_ARRAY, filters.toTypedArray())
.putExtra(EXTRA_KEY_PAGE, "/collection")
.putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray())
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
// so we use a joined `String` as fallback
.putExtra(SHORTCUT_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
else -> {
result.error("pin-intent", "failed to build intent", null)
return

View file

@ -22,9 +22,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
// starting activity to give access with the native dialog
// starting activity to get a result (e.g. storage access via native dialog)
// breaks the regular `MethodChannel` so we use a stream channel instead
class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?) : EventChannel.StreamHandler {
class ActivityResultStreamHandler(private val activity: Activity, arguments: Any?) : EventChannel.StreamHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private lateinit var eventSink: EventSink
private lateinit var handler: Handler
@ -48,6 +48,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
"requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() }
"createFile" -> ioScope.launch { createFile() }
"openFile" -> ioScope.launch { openFile() }
"pickCollectionFilters" -> pickCollectionFilters()
else -> endOfStream()
}
}
@ -186,6 +187,18 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
}
}
private fun pickCollectionFilters() {
val initialFilters = (args["initialFilters"] as List<*>?)?.mapNotNull { if (it is String) it else null } ?: listOf()
val intent = Intent(MainActivity.INTENT_ACTION_PICK_COLLECTION_FILTERS, null, activity, MainActivity::class.java)
.putExtra(MainActivity.EXTRA_KEY_FILTERS_ARRAY, initialFilters.toTypedArray())
.putExtra(MainActivity.EXTRA_KEY_FILTERS_STRING, initialFilters.joinToString(MainActivity.EXTRA_STRING_ARRAY_SEPARATOR))
MainActivity.pendingCollectionFilterPickHandler = { filters ->
success(filters)
endOfStream()
}
activity.startActivityForResult(intent, MainActivity.PICK_COLLECTION_FILTERS_REQUEST)
}
override fun onCancel(arguments: Any?) {}
private fun success(result: Any?) {
@ -221,8 +234,8 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
}
companion object {
private val LOG_TAG = LogUtils.createTag<StorageAccessStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/storage_access_stream"
private val LOG_TAG = LogUtils.createTag<ActivityResultStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/activity_result_stream"
private const val BUFFER_SIZE = 2 shl 17 // 256kB
}
}

View file

@ -20,6 +20,6 @@ class IntentStreamHandler : EventChannel.StreamHandler {
}
companion object {
const val CHANNEL = "deckers.thibault/aves/intent"
const val CHANNEL = "deckers.thibault/aves/new_intent_stream"
}
}

View file

@ -1,5 +1,6 @@
enum AppMode {
main,
pickCollectionFiltersExternal,
pickSingleMediaExternal,
pickMultipleMediaExternal,
pickMediaInternal,
@ -11,13 +12,23 @@ enum AppMode {
}
extension ExtraAppMode on AppMode {
bool get canSearch => this == AppMode.main || this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal;
bool get canNavigate => {
AppMode.main,
AppMode.pickCollectionFiltersExternal,
AppMode.pickSingleMediaExternal,
AppMode.pickMultipleMediaExternal,
}.contains(this);
bool get canSelectMedia => this == AppMode.main || this == AppMode.pickMultipleMediaExternal;
bool get canSelectMedia => {
AppMode.main,
AppMode.pickMultipleMediaExternal,
}.contains(this);
bool get canSelectFilter => this == AppMode.main;
bool get hasDrawer => this == AppMode.main || this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal;
bool get isPickingMedia => this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal || this == AppMode.pickMediaInternal;
bool get isPickingMedia => {
AppMode.pickSingleMediaExternal,
AppMode.pickMultipleMediaExternal,
AppMode.pickMediaInternal,
}.contains(this);
}

View file

@ -31,7 +31,7 @@ class SettingsDefaults {
static const mustBackTwiceToExit = true;
static const keepScreenOn = KeepScreenOn.viewerOnly;
static const homePage = HomePageSetting.collection;
static const showBottomNavigationBar = true;
static const enableBottomNavigationBar = true;
static const confirmDeleteForever = true;
static const confirmMoveToBin = true;
static const confirmMoveUndatedItems = true;

View file

@ -60,7 +60,7 @@ class Settings extends ChangeNotifier {
static const mustBackTwiceToExitKey = 'must_back_twice_to_exit';
static const keepScreenOnKey = 'keep_screen_on';
static const homePageKey = 'home_page';
static const showBottomNavigationBarKey = 'show_bottom_navigation_bar';
static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar';
static const confirmDeleteForeverKey = 'confirm_delete_forever';
static const confirmMoveToBinKey = 'confirm_move_to_bin';
static const confirmMoveUndatedItemsKey = 'confirm_move_undated_items';
@ -142,6 +142,7 @@ class Settings extends ChangeNotifier {
static const screenSaverTransitionKey = 'screen_saver_transition';
static const screenSaverVideoPlaybackKey = 'screen_saver_video_playback';
static const screenSaverIntervalKey = 'screen_saver_interval';
static const screenSaverCollectionFiltersKey = 'screen_saver_collection_filters';
// slideshow
static const slideshowRepeatKey = 'slideshow_loop';
@ -320,9 +321,9 @@ class Settings extends ChangeNotifier {
set homePage(HomePageSetting newValue) => setAndNotify(homePageKey, newValue.toString());
bool get showBottomNavigationBar => getBoolOrDefault(showBottomNavigationBarKey, SettingsDefaults.showBottomNavigationBar);
bool get enableBottomNavigationBar => getBoolOrDefault(enableBottomNavigationBarKey, SettingsDefaults.enableBottomNavigationBar);
set showBottomNavigationBar(bool newValue) => setAndNotify(showBottomNavigationBarKey, newValue);
set enableBottomNavigationBar(bool newValue) => setAndNotify(enableBottomNavigationBarKey, newValue);
bool get confirmDeleteForever => getBoolOrDefault(confirmDeleteForeverKey, SettingsDefaults.confirmDeleteForever);
@ -602,6 +603,10 @@ class Settings extends ChangeNotifier {
set screenSaverInterval(SlideshowInterval newValue) => setAndNotify(screenSaverIntervalKey, newValue.toString());
Set<CollectionFilter> get screenSaverCollectionFilters => (getStringList(screenSaverCollectionFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
set screenSaverCollectionFilters(Set<CollectionFilter> newValue) => setAndNotify(screenSaverCollectionFiltersKey, newValue.map((filter) => filter.toJson()).toList());
// slideshow
bool get slideshowRepeat => getBoolOrDefault(slideshowRepeatKey, SettingsDefaults.slideshowRepeat);
@ -754,7 +759,7 @@ class Settings extends ChangeNotifier {
case isErrorReportingAllowedKey:
case enableDynamicColorKey:
case enableBlurEffectKey:
case showBottomNavigationBarKey:
case enableBottomNavigationBarKey:
case mustBackTwiceToExitKey:
case confirmDeleteForeverKey:
case confirmMoveToBinKey:
@ -831,6 +836,7 @@ class Settings extends ChangeNotifier {
case collectionBrowsingQuickActionsKey:
case collectionSelectionQuickActionsKey:
case viewerQuickActionsKey:
case screenSaverCollectionFiltersKey:
if (newValue is List) {
settingsStore.setStringList(key, newValue.cast<String>());
} else {

View file

@ -2,11 +2,11 @@ import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart';
class AccessibilityService {
static const platform = MethodChannel('deckers.thibault/aves/accessibility');
static const _platform = MethodChannel('deckers.thibault/aves/accessibility');
static Future<bool> areAnimationsRemoved() async {
try {
final result = await platform.invokeMethod('areAnimationsRemoved');
final result = await _platform.invokeMethod('areAnimationsRemoved');
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
@ -16,7 +16,7 @@ class AccessibilityService {
static Future<bool> hasRecommendedTimeouts() async {
try {
final result = await platform.invokeMethod('hasRecommendedTimeouts');
final result = await _platform.invokeMethod('hasRecommendedTimeouts');
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
@ -26,7 +26,7 @@ class AccessibilityService {
static Future<int> getRecommendedTimeToRead(int originalTimeoutMillis) async {
try {
final result = await platform.invokeMethod('getRecommendedTimeoutMillis', <String, dynamic>{
final result = await _platform.invokeMethod('getRecommendedTimeoutMillis', <String, dynamic>{
'originalTimeoutMillis': originalTimeoutMillis,
'content': ['icons', 'text']
});
@ -39,7 +39,7 @@ class AccessibilityService {
static Future<int> getRecommendedTimeToTakeAction(int originalTimeoutMillis) async {
try {
final result = await platform.invokeMethod('getRecommendedTimeoutMillis', <String, dynamic>{
final result = await _platform.invokeMethod('getRecommendedTimeoutMillis', <String, dynamic>{
'originalTimeoutMillis': originalTimeoutMillis,
'content': ['controls', 'icons', 'text']
});

View file

@ -13,11 +13,11 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
class AnalysisService {
static const platform = MethodChannel('deckers.thibault/aves/analysis');
static const _platform = MethodChannel('deckers.thibault/aves/analysis');
static Future<void> registerCallback() async {
try {
await platform.invokeMethod('registerCallback', <String, dynamic>{
await _platform.invokeMethod('registerCallback', <String, dynamic>{
'callbackHandle': PluginUtilities.getCallbackHandle(_init)?.toRawHandle(),
});
} on PlatformException catch (e, stack) {
@ -27,7 +27,7 @@ class AnalysisService {
static Future<void> startService({required bool force, List<int>? entryIds}) async {
try {
await platform.invokeMethod('startService', <String, dynamic>{
await _platform.invokeMethod('startService', <String, dynamic>{
'entryIds': entryIds,
'force': force,
});

View file

@ -34,9 +34,9 @@ abstract class AndroidAppService {
}
class PlatformAndroidAppService implements AndroidAppService {
static const platform = MethodChannel('deckers.thibault/aves/app');
static const _platform = MethodChannel('deckers.thibault/aves/app');
static final knownAppDirs = {
static final _knownAppDirs = {
'com.kakao.talk': {'KakaoTalkDownload'},
'com.sony.playmemories.mobile': {'Imaging Edge Mobile'},
'nekox.messenger': {'NekoX'},
@ -45,10 +45,10 @@ class PlatformAndroidAppService implements AndroidAppService {
@override
Future<Set<Package>> getPackages() async {
try {
final result = await platform.invokeMethod('getPackages');
final result = await _platform.invokeMethod('getPackages');
final packages = (result as List).cast<Map>().map(Package.fromMap).toSet();
// additional info for known directories
knownAppDirs.forEach((packageName, dirs) {
_knownAppDirs.forEach((packageName, dirs) {
final package = packages.firstWhereOrNull((package) => package.packageName == packageName);
if (package != null) {
package.ownedDirs.addAll(dirs);
@ -64,7 +64,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override
Future<Uint8List> getAppIcon(String packageName, double size) async {
try {
final result = await platform.invokeMethod('getAppIcon', <String, dynamic>{
final result = await _platform.invokeMethod('getAppIcon', <String, dynamic>{
'packageName': packageName,
'sizeDip': size,
});
@ -78,7 +78,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override
Future<String?> getAppInstaller() async {
try {
return await platform.invokeMethod('getAppInstaller');
return await _platform.invokeMethod('getAppInstaller');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
@ -88,7 +88,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override
Future<bool> copyToClipboard(String uri, String? label) async {
try {
final result = await platform.invokeMethod('copyToClipboard', <String, dynamic>{
final result = await _platform.invokeMethod('copyToClipboard', <String, dynamic>{
'uri': uri,
'label': label,
});
@ -102,7 +102,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override
Future<bool> edit(String uri, String mimeType) async {
try {
final result = await platform.invokeMethod('edit', <String, dynamic>{
final result = await _platform.invokeMethod('edit', <String, dynamic>{
'uri': uri,
'mimeType': mimeType,
});
@ -116,7 +116,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override
Future<bool> open(String uri, String mimeType) async {
try {
final result = await platform.invokeMethod('open', <String, dynamic>{
final result = await _platform.invokeMethod('open', <String, dynamic>{
'uri': uri,
'mimeType': mimeType,
});
@ -134,7 +134,7 @@ class PlatformAndroidAppService implements AndroidAppService {
final geoUri = 'geo:$latitude,$longitude?q=$latitude,$longitude';
try {
final result = await platform.invokeMethod('openMap', <String, dynamic>{
final result = await _platform.invokeMethod('openMap', <String, dynamic>{
'geoUri': geoUri,
});
if (result != null) return result as bool;
@ -147,7 +147,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override
Future<bool> setAs(String uri, String mimeType) async {
try {
final result = await platform.invokeMethod('setAs', <String, dynamic>{
final result = await _platform.invokeMethod('setAs', <String, dynamic>{
'uri': uri,
'mimeType': mimeType,
});
@ -164,7 +164,7 @@ class PlatformAndroidAppService implements AndroidAppService {
// e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats
final urisByMimeType = groupBy<AvesEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
try {
final result = await platform.invokeMethod('share', <String, dynamic>{
final result = await _platform.invokeMethod('share', <String, dynamic>{
'urisByMimeType': urisByMimeType,
});
if (result != null) return result as bool;
@ -177,7 +177,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override
Future<bool> shareSingle(String uri, String mimeType) async {
try {
final result = await platform.invokeMethod('share', <String, dynamic>{
final result = await _platform.invokeMethod('share', <String, dynamic>{
'urisByMimeType': {
mimeType: [uri]
},
@ -207,7 +207,7 @@ class PlatformAndroidAppService implements AndroidAppService {
);
}
try {
await platform.invokeMethod('pinShortcut', <String, dynamic>{
await _platform.invokeMethod('pinShortcut', <String, dynamic>{
'label': label,
'iconBytes': iconBytes,
'filters': filters?.map((filter) => filter.toJson()).toList(),

View file

@ -4,11 +4,11 @@ import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart';
class AndroidDebugService {
static const platform = MethodChannel('deckers.thibault/aves/debug');
static const _platform = MethodChannel('deckers.thibault/aves/debug');
static Future<void> crash() async {
try {
await platform.invokeMethod('crash');
await _platform.invokeMethod('crash');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
@ -16,7 +16,7 @@ class AndroidDebugService {
static Future<void> exception() async {
try {
await platform.invokeMethod('exception');
await _platform.invokeMethod('exception');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
@ -24,7 +24,7 @@ class AndroidDebugService {
static Future<void> safeException() async {
try {
await platform.invokeMethod('safeException');
await _platform.invokeMethod('safeException');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
@ -32,7 +32,7 @@ class AndroidDebugService {
static Future<void> exceptionInCoroutine() async {
try {
await platform.invokeMethod('exceptionInCoroutine');
await _platform.invokeMethod('exceptionInCoroutine');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
@ -40,7 +40,7 @@ class AndroidDebugService {
static Future<void> safeExceptionInCoroutine() async {
try {
await platform.invokeMethod('safeExceptionInCoroutine');
await _platform.invokeMethod('safeExceptionInCoroutine');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
@ -48,7 +48,7 @@ class AndroidDebugService {
static Future<Map> getContextDirs() async {
try {
final result = await platform.invokeMethod('getContextDirs');
final result = await _platform.invokeMethod('getContextDirs');
if (result != null) return result as Map;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
@ -58,7 +58,7 @@ class AndroidDebugService {
static Future<List<Map>> getCodecs() async {
try {
final result = await platform.invokeMethod('getCodecs');
final result = await _platform.invokeMethod('getCodecs');
if (result != null) return (result as List).cast<Map>();
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
@ -68,7 +68,7 @@ class AndroidDebugService {
static Future<Map> getEnv() async {
try {
final result = await platform.invokeMethod('getEnv');
final result = await _platform.invokeMethod('getEnv');
if (result != null) return result as Map;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
@ -79,7 +79,7 @@ class AndroidDebugService {
static Future<Map> getBitmapFactoryInfo(AvesEntry entry) async {
try {
// returns map with all data available when decoding image bounds with `BitmapFactory`
final result = await platform.invokeMethod('getBitmapFactoryInfo', <String, dynamic>{
final result = await _platform.invokeMethod('getBitmapFactoryInfo', <String, dynamic>{
'uri': entry.uri,
});
if (result != null) return result as Map;
@ -92,7 +92,7 @@ class AndroidDebugService {
static Future<Map> getContentResolverMetadata(AvesEntry entry) async {
try {
// returns map with all data available from the content resolver
final result = await platform.invokeMethod('getContentResolverMetadata', <String, dynamic>{
final result = await _platform.invokeMethod('getContentResolverMetadata', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
});
@ -106,7 +106,7 @@ class AndroidDebugService {
static Future<Map> getExifInterfaceMetadata(AvesEntry entry) async {
try {
// returns map with all data available from the `ExifInterface` library
final result = await platform.invokeMethod('getExifInterfaceMetadata', <String, dynamic>{
final result = await _platform.invokeMethod('getExifInterfaceMetadata', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,
@ -121,7 +121,7 @@ class AndroidDebugService {
static Future<Map> getMediaMetadataRetrieverMetadata(AvesEntry entry) async {
try {
// returns map with all data available from `MediaMetadataRetriever`
final result = await platform.invokeMethod('getMediaMetadataRetrieverMetadata', <String, dynamic>{
final result = await _platform.invokeMethod('getMediaMetadataRetrieverMetadata', <String, dynamic>{
'uri': entry.uri,
});
if (result != null) return result as Map;
@ -134,7 +134,7 @@ class AndroidDebugService {
static Future<Map> getMetadataExtractorSummary(AvesEntry entry) async {
try {
// returns map with the MIME type and tag count for each directory found by `metadata-extractor`
final result = await platform.invokeMethod('getMetadataExtractorSummary', <String, dynamic>{
final result = await _platform.invokeMethod('getMetadataExtractorSummary', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,
@ -149,7 +149,7 @@ class AndroidDebugService {
static Future<Map> getPixyMetadata(AvesEntry entry) async {
try {
// returns map with all data available from the `PixyMeta` library
final result = await platform.invokeMethod('getPixyMetadata', <String, dynamic>{
final result = await _platform.invokeMethod('getPixyMetadata', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
});
@ -164,7 +164,7 @@ class AndroidDebugService {
if (entry.mimeType != MimeTypes.tiff) return {};
try {
final result = await platform.invokeMethod('getTiffStructure', <String, dynamic>{
final result = await _platform.invokeMethod('getTiffStructure', <String, dynamic>{
'uri': entry.uri,
});
if (result != null) return result as Map;

View file

@ -16,12 +16,12 @@ abstract class DeviceService {
}
class PlatformDeviceService implements DeviceService {
static const platform = MethodChannel('deckers.thibault/aves/device');
static const _platform = MethodChannel('deckers.thibault/aves/device');
@override
Future<Map<String, dynamic>> getCapabilities() async {
try {
final result = await platform.invokeMethod('getCapabilities');
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);
@ -32,7 +32,7 @@ class PlatformDeviceService implements DeviceService {
@override
Future<String?> getDefaultTimeZone() async {
try {
return await platform.invokeMethod('getDefaultTimeZone');
return await _platform.invokeMethod('getDefaultTimeZone');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
@ -42,7 +42,7 @@ class PlatformDeviceService implements DeviceService {
@override
Future<List<Locale>> getLocales() async {
try {
final result = await platform.invokeMethod('getLocales');
final result = await _platform.invokeMethod('getLocales');
if (result != null) {
return (result as List).cast<Map>().map((tags) {
final language = tags['language'] as String?;
@ -62,7 +62,7 @@ class PlatformDeviceService implements DeviceService {
@override
Future<int> getPerformanceClass() async {
try {
final result = await platform.invokeMethod('getPerformanceClass');
final result = await _platform.invokeMethod('getPerformanceClass');
if (result != null) return result as int;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
@ -73,7 +73,7 @@ class PlatformDeviceService implements DeviceService {
@override
Future<bool> isSystemFilePickerEnabled() async {
try {
final result = await platform.invokeMethod('isSystemFilePickerEnabled');
final result = await _platform.invokeMethod('isSystemFilePickerEnabled');
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);

View file

@ -7,12 +7,12 @@ import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart';
class GeocodingService {
static const platform = MethodChannel('deckers.thibault/aves/geocoding');
static const _platform = MethodChannel('deckers.thibault/aves/geocoding');
// geocoding requires Google Play Services
static Future<List<Address>> getAddress(LatLng coordinates, Locale locale) async {
try {
final result = await platform.invokeMethod('getAddress', <String, dynamic>{
final result = await _platform.invokeMethod('getAddress', <String, dynamic>{
'latitude': coordinates.latitude,
'longitude': coordinates.longitude,
'locale': locale.toString(),

View file

@ -7,11 +7,11 @@ import 'package:flutter/widgets.dart';
import 'package:intl/date_symbol_data_local.dart';
class GlobalSearch {
static const platform = MethodChannel('deckers.thibault/aves/global_search');
static const _platform = MethodChannel('deckers.thibault/aves/global_search');
static Future<void> registerCallback() async {
try {
await platform.invokeMethod('registerCallback', <String, dynamic>{
await _platform.invokeMethod('registerCallback', <String, dynamic>{
'callbackHandle': PluginUtilities.getCallbackHandle(_init)?.toRawHandle(),
});
} on PlatformException catch (e, stack) {

View file

@ -0,0 +1,68 @@
import 'dart:async';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart';
import 'package:flutter/services.dart';
import 'package:streams_channel/streams_channel.dart';
class IntentService {
static const _platform = MethodChannel('deckers.thibault/aves/intent');
static final _stream = StreamsChannel('deckers.thibault/aves/activity_result_stream');
static Future<Map<String, dynamic>> getIntentData() async {
try {
// returns nullable map with 'action' and possibly 'uri' 'mimeType'
final result = await _platform.invokeMethod('getIntentData');
if (result != null) return (result as Map).cast<String, dynamic>();
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return {};
}
static Future<void> submitPickedItems(List<String> uris) async {
try {
await _platform.invokeMethod('submitPickedItems', <String, dynamic>{
'uris': uris,
});
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
}
static Future<void> submitPickedCollectionFilters(Set<CollectionFilter>? filters) async {
try {
await _platform.invokeMethod('submitPickedCollectionFilters', <String, dynamic>{
'filters': filters?.map((filter) => filter.toJson()).toList(),
});
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
}
static Future<Set<CollectionFilter>?> pickCollectionFilters(Set<CollectionFilter>? initialFilters) async {
try {
final completer = Completer<Set<CollectionFilter>?>();
_stream.receiveBroadcastStream(<String, dynamic>{
'op': 'pickCollectionFilters',
'initialFilters': initialFilters?.map((filter) => filter.toJson()).toList(),
}).listen(
(data) {
final result = (data as List?)?.cast<String>().map(CollectionFilter.fromJson).whereNotNull().toSet();
completer.complete(result);
},
onError: completer.completeError,
onDone: () {
if (!completer.isCompleted) completer.complete(null);
},
cancelOnError: true,
);
// `await` here, so that `completeError` will be caught below
return await completer.future;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return null;
}
}

View file

@ -15,12 +15,12 @@ abstract class EmbeddedDataService {
}
class PlatformEmbeddedDataService implements EmbeddedDataService {
static const platform = MethodChannel('deckers.thibault/aves/embedded');
static const _platform = MethodChannel('deckers.thibault/aves/embedded');
@override
Future<List<Uint8List>> getExifThumbnails(AvesEntry entry) async {
try {
final result = await platform.invokeMethod('getExifThumbnails', <String, dynamic>{
final result = await _platform.invokeMethod('getExifThumbnails', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,
@ -35,7 +35,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
@override
Future<Map> extractMotionPhotoVideo(AvesEntry entry) async {
try {
final result = await platform.invokeMethod('extractMotionPhotoVideo', <String, dynamic>{
final result = await _platform.invokeMethod('extractMotionPhotoVideo', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,
@ -51,7 +51,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
@override
Future<Map> extractVideoEmbeddedPicture(AvesEntry entry) async {
try {
final result = await platform.invokeMethod('extractVideoEmbeddedPicture', <String, dynamic>{
final result = await _platform.invokeMethod('extractVideoEmbeddedPicture', <String, dynamic>{
'uri': entry.uri,
'displayName': '${entry.bestTitle} • Cover',
});
@ -65,7 +65,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
@override
Future<Map> extractXmpDataProp(AvesEntry entry, String? propPath, String? propMimeType) async {
try {
final result = await platform.invokeMethod('extractXmpDataProp', <String, dynamic>{
final result = await _platform.invokeMethod('extractXmpDataProp', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,

View file

@ -108,10 +108,10 @@ abstract class MediaFileService {
}
class PlatformMediaFileService implements MediaFileService {
static const platform = MethodChannel('deckers.thibault/aves/media_file');
static final StreamsChannel _byteStreamChannel = StreamsChannel('deckers.thibault/aves/media_byte_stream');
static final StreamsChannel _opStreamChannel = StreamsChannel('deckers.thibault/aves/media_op_stream');
static const double thumbnailDefaultSize = 64.0;
static const _platform = MethodChannel('deckers.thibault/aves/media_file');
static final _byteStream = StreamsChannel('deckers.thibault/aves/media_byte_stream');
static final _opStream = StreamsChannel('deckers.thibault/aves/media_op_stream');
static const double _thumbnailDefaultSize = 64.0;
static Map<String, dynamic> _toPlatformEntryMap(AvesEntry entry) {
return {
@ -136,7 +136,7 @@ class PlatformMediaFileService implements MediaFileService {
@override
Future<AvesEntry?> getEntry(String uri, String? mimeType) async {
try {
final result = await platform.invokeMethod('getEntry', <String, dynamic>{
final result = await _platform.invokeMethod('getEntry', <String, dynamic>{
'uri': uri,
'mimeType': mimeType,
}) as Map;
@ -181,7 +181,7 @@ class PlatformMediaFileService implements MediaFileService {
final completer = Completer<Uint8List>.sync();
final sink = OutputBuffer();
var bytesReceived = 0;
_byteStreamChannel.receiveBroadcastStream(<String, dynamic>{
_byteStream.receiveBroadcastStream(<String, dynamic>{
'uri': uri,
'mimeType': mimeType,
'rotationDegrees': rotationDegrees ?? 0,
@ -234,7 +234,7 @@ class PlatformMediaFileService implements MediaFileService {
return servicePolicy.call(
() async {
try {
final result = await platform.invokeMethod('getRegion', <String, dynamic>{
final result = await _platform.invokeMethod('getRegion', <String, dynamic>{
'uri': uri,
'mimeType': mimeType,
'pageId': pageId,
@ -274,7 +274,7 @@ class PlatformMediaFileService implements MediaFileService {
return servicePolicy.call(
() async {
try {
final result = await platform.invokeMethod('getThumbnail', <String, dynamic>{
final result = await _platform.invokeMethod('getThumbnail', <String, dynamic>{
'uri': uri,
'mimeType': mimeType,
'dateModifiedSecs': dateModifiedSecs,
@ -283,7 +283,7 @@ class PlatformMediaFileService implements MediaFileService {
'widthDip': extent,
'heightDip': extent,
'pageId': pageId,
'defaultSizeDip': thumbnailDefaultSize,
'defaultSizeDip': _thumbnailDefaultSize,
});
if (result != null) return result as Uint8List;
} on PlatformException catch (e, stack) {
@ -301,7 +301,7 @@ class PlatformMediaFileService implements MediaFileService {
@override
Future<void> clearSizedThumbnailDiskCache() async {
try {
return platform.invokeMethod('clearSizedThumbnailDiskCache');
return _platform.invokeMethod('clearSizedThumbnailDiskCache');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
@ -319,7 +319,7 @@ class PlatformMediaFileService implements MediaFileService {
@override
Future<void> cancelFileOp(String opId) async {
try {
await platform.invokeMethod('cancelFileOp', <String, dynamic>{
await _platform.invokeMethod('cancelFileOp', <String, dynamic>{
'opId': opId,
});
} on PlatformException catch (e, stack) {
@ -333,7 +333,7 @@ class PlatformMediaFileService implements MediaFileService {
required Iterable<AvesEntry> entries,
}) {
try {
return _opStreamChannel
return _opStream
.receiveBroadcastStream(<String, dynamic>{
'op': 'delete',
'id': opId,
@ -355,7 +355,7 @@ class PlatformMediaFileService implements MediaFileService {
required NameConflictStrategy nameConflictStrategy,
}) {
try {
return _opStreamChannel
return _opStream
.receiveBroadcastStream(<String, dynamic>{
'op': 'move',
'id': opId,
@ -379,7 +379,7 @@ class PlatformMediaFileService implements MediaFileService {
required NameConflictStrategy nameConflictStrategy,
}) {
try {
return _opStreamChannel
return _opStream
.receiveBroadcastStream(<String, dynamic>{
'op': 'export',
'entries': entries.map(_toPlatformEntryMap).toList(),
@ -403,7 +403,7 @@ class PlatformMediaFileService implements MediaFileService {
required Map<AvesEntry, String> entriesToNewName,
}) {
try {
return _opStreamChannel
return _opStream
.receiveBroadcastStream(<String, dynamic>{
'op': 'rename',
'id': opId,
@ -427,7 +427,7 @@ class PlatformMediaFileService implements MediaFileService {
required NameConflictStrategy nameConflictStrategy,
}) async {
try {
final result = await platform.invokeMethod('captureFrame', <String, dynamic>{
final result = await _platform.invokeMethod('captureFrame', <String, dynamic>{
'uri': entry.uri,
'desiredName': desiredName,
'exif': exif,

View file

@ -18,13 +18,13 @@ abstract class MediaStoreService {
}
class PlatformMediaStoreService implements MediaStoreService {
static const platform = MethodChannel('deckers.thibault/aves/media_store');
static final StreamsChannel _streamChannel = StreamsChannel('deckers.thibault/aves/media_store_stream');
static const _platform = MethodChannel('deckers.thibault/aves/media_store');
static final _stream = StreamsChannel('deckers.thibault/aves/media_store_stream');
@override
Future<List<int>> checkObsoleteContentIds(List<int?> knownContentIds) async {
try {
final result = await platform.invokeMethod('checkObsoleteContentIds', <String, dynamic>{
final result = await _platform.invokeMethod('checkObsoleteContentIds', <String, dynamic>{
'knownContentIds': knownContentIds,
});
return (result as List).cast<int>();
@ -37,7 +37,7 @@ class PlatformMediaStoreService implements MediaStoreService {
@override
Future<List<int>> checkObsoletePaths(Map<int?, String?> knownPathById) async {
try {
final result = await platform.invokeMethod('checkObsoletePaths', <String, dynamic>{
final result = await _platform.invokeMethod('checkObsoletePaths', <String, dynamic>{
'knownPathById': knownPathById,
});
return (result as List).cast<int>();
@ -50,7 +50,7 @@ class PlatformMediaStoreService implements MediaStoreService {
@override
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) {
try {
return _streamChannel
return _stream
.receiveBroadcastStream(<String, dynamic>{
'knownEntries': knownEntries,
'directory': directory,
@ -67,7 +67,7 @@ class PlatformMediaStoreService implements MediaStoreService {
@override
Future<Uri?> scanFile(String path, String mimeType) async {
try {
final result = await platform.invokeMethod('scanFile', <String, dynamic>{
final result = await _platform.invokeMethod('scanFile', <String, dynamic>{
'path': path,
'mimeType': mimeType,
});

View file

@ -23,7 +23,7 @@ abstract class MetadataEditService {
}
class PlatformMetadataEditService implements MetadataEditService {
static const platform = MethodChannel('deckers.thibault/aves/metadata_edit');
static const _platform = MethodChannel('deckers.thibault/aves/metadata_edit');
static Map<String, dynamic> _toPlatformEntryMap(AvesEntry entry) {
return {
@ -44,7 +44,7 @@ class PlatformMetadataEditService implements MetadataEditService {
Future<Map<String, dynamic>> rotate(AvesEntry entry, {required bool clockwise}) async {
try {
// returns map with: 'rotationDegrees' 'isFlipped'
final result = await platform.invokeMethod('rotate', <String, dynamic>{
final result = await _platform.invokeMethod('rotate', <String, dynamic>{
'entry': _toPlatformEntryMap(entry),
'clockwise': clockwise,
});
@ -61,7 +61,7 @@ class PlatformMetadataEditService implements MetadataEditService {
Future<Map<String, dynamic>> flip(AvesEntry entry) async {
try {
// returns map with: 'rotationDegrees' 'isFlipped'
final result = await platform.invokeMethod('flip', <String, dynamic>{
final result = await _platform.invokeMethod('flip', <String, dynamic>{
'entry': _toPlatformEntryMap(entry),
});
if (result != null) return (result as Map).cast<String, dynamic>();
@ -76,7 +76,7 @@ class PlatformMetadataEditService implements MetadataEditService {
@override
Future<Map<String, dynamic>> editExifDate(AvesEntry entry, DateModifier modifier) async {
try {
final result = await platform.invokeMethod('editDate', <String, dynamic>{
final result = await _platform.invokeMethod('editDate', <String, dynamic>{
'entry': _toPlatformEntryMap(entry),
'dateMillis': modifier.setDateTime?.millisecondsSinceEpoch,
'shiftMinutes': modifier.shiftMinutes,
@ -98,7 +98,7 @@ class PlatformMetadataEditService implements MetadataEditService {
bool autoCorrectTrailerOffset = true,
}) async {
try {
final result = await platform.invokeMethod('editMetadata', <String, dynamic>{
final result = await _platform.invokeMethod('editMetadata', <String, dynamic>{
'entry': _toPlatformEntryMap(entry),
'metadata': metadata.map((type, value) => MapEntry(_toPlatformMetadataType(type), value)),
'autoCorrectTrailerOffset': autoCorrectTrailerOffset,
@ -115,7 +115,7 @@ class PlatformMetadataEditService implements MetadataEditService {
@override
Future<Map<String, dynamic>> removeTrailerVideo(AvesEntry entry) async {
try {
final result = await platform.invokeMethod('removeTrailerVideo', <String, dynamic>{
final result = await _platform.invokeMethod('removeTrailerVideo', <String, dynamic>{
'entry': _toPlatformEntryMap(entry),
});
if (result != null) return (result as Map).cast<String, dynamic>();
@ -130,7 +130,7 @@ class PlatformMetadataEditService implements MetadataEditService {
@override
Future<Map<String, dynamic>> removeTypes(AvesEntry entry, Set<MetadataType> types) async {
try {
final result = await platform.invokeMethod('removeTypes', <String, dynamic>{
final result = await _platform.invokeMethod('removeTypes', <String, dynamic>{
'entry': _toPlatformEntryMap(entry),
'types': types.map(_toPlatformMetadataType).toList(),
});

View file

@ -38,14 +38,14 @@ abstract class MetadataFetchService {
}
class PlatformMetadataFetchService implements MetadataFetchService {
static const platform = MethodChannel('deckers.thibault/aves/metadata_fetch');
static const _platform = MethodChannel('deckers.thibault/aves/metadata_fetch');
@override
Future<Map> getAllMetadata(AvesEntry entry) async {
if (entry.isSvg) return {};
try {
final result = await platform.invokeMethod('getAllMetadata', <String, dynamic>{
final result = await _platform.invokeMethod('getAllMetadata', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,
@ -76,7 +76,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
// 'longitude': longitude (double)
// 'xmpSubjects': ';' separated XMP subjects (string)
// 'xmpTitleDescription': XMP title or XMP description (string)
final result = await platform.invokeMethod('getCatalogMetadata', <String, dynamic>{
final result = await _platform.invokeMethod('getCatalogMetadata', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'path': entry.path,
@ -106,7 +106,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
try {
// returns map with values for: 'aperture' (double), 'exposureTime' (description), 'focalLength' (double), 'iso' (int)
final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
final result = await _platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,
@ -123,7 +123,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
@override
Future<GeoTiffInfo?> getGeoTiffInfo(AvesEntry entry) async {
try {
final result = await platform.invokeMethod('getGeoTiffInfo', <String, dynamic>{
final result = await _platform.invokeMethod('getGeoTiffInfo', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,
@ -140,7 +140,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
@override
Future<MultiPageInfo?> getMultiPageInfo(AvesEntry entry) async {
try {
final result = await platform.invokeMethod('getMultiPageInfo', <String, dynamic>{
final result = await _platform.invokeMethod('getMultiPageInfo', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,
@ -167,7 +167,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
// returns map with values for:
// 'croppedAreaLeft' (int), 'croppedAreaTop' (int), 'croppedAreaWidth' (int), 'croppedAreaHeight' (int),
// 'fullPanoWidth' (int), 'fullPanoHeight' (int)
final result = await platform.invokeMethod('getPanoramaInfo', <String, dynamic>{
final result = await _platform.invokeMethod('getPanoramaInfo', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,
@ -184,7 +184,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
@override
Future<List<Map<String, dynamic>>?> getIptc(AvesEntry entry) async {
try {
final result = await platform.invokeMethod('getIptc', <String, dynamic>{
final result = await _platform.invokeMethod('getIptc', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
});
@ -200,7 +200,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
@override
Future<AvesXmp?> getXmp(AvesEntry entry) async {
try {
final result = await platform.invokeMethod('getXmp', <String, dynamic>{
final result = await _platform.invokeMethod('getXmp', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,
@ -222,7 +222,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
if (exists != null) return SynchronousFuture(exists);
try {
exists = await platform.invokeMethod('hasContentResolverProp', <String, dynamic>{
exists = await _platform.invokeMethod('hasContentResolverProp', <String, dynamic>{
'prop': prop,
});
} on PlatformException catch (e, stack) {
@ -236,7 +236,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
@override
Future<String?> getContentResolverProp(AvesEntry entry, String prop) async {
try {
return await platform.invokeMethod('getContentResolverProp', <String, dynamic>{
return await _platform.invokeMethod('getContentResolverProp', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'prop': prop,
@ -252,7 +252,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
@override
Future<DateTime?> getDate(AvesEntry entry, MetadataField field) async {
try {
final result = await platform.invokeMethod('getDate', <String, dynamic>{
final result = await _platform.invokeMethod('getDate', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'sizeBytes': entry.sizeBytes,

View file

@ -40,13 +40,13 @@ abstract class StorageService {
}
class PlatformStorageService implements StorageService {
static const platform = MethodChannel('deckers.thibault/aves/storage');
static final StreamsChannel storageAccessChannel = StreamsChannel('deckers.thibault/aves/storage_access_stream');
static const _platform = MethodChannel('deckers.thibault/aves/storage');
static final _stream = StreamsChannel('deckers.thibault/aves/activity_result_stream');
@override
Future<Set<StorageVolume>> getStorageVolumes() async {
try {
final result = await platform.invokeMethod('getStorageVolumes');
final result = await _platform.invokeMethod('getStorageVolumes');
return (result as List).cast<Map>().map(StorageVolume.fromMap).toSet();
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
@ -57,7 +57,7 @@ class PlatformStorageService implements StorageService {
@override
Future<int?> getFreeSpace(StorageVolume volume) async {
try {
final result = await platform.invokeMethod('getFreeSpace', <String, dynamic>{
final result = await _platform.invokeMethod('getFreeSpace', <String, dynamic>{
'path': volume.path,
});
return result as int?;
@ -70,7 +70,7 @@ class PlatformStorageService implements StorageService {
@override
Future<List<String>> getGrantedDirectories() async {
try {
final result = await platform.invokeMethod('getGrantedDirectories');
final result = await _platform.invokeMethod('getGrantedDirectories');
return (result as List).cast<String>();
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
@ -81,7 +81,7 @@ class PlatformStorageService implements StorageService {
@override
Future<Set<VolumeRelativeDirectory>> getInaccessibleDirectories(Iterable<String> dirPaths) async {
try {
final result = await platform.invokeMethod('getInaccessibleDirectories', <String, dynamic>{
final result = await _platform.invokeMethod('getInaccessibleDirectories', <String, dynamic>{
'dirPaths': dirPaths.toList(),
});
if (result != null) {
@ -96,7 +96,7 @@ class PlatformStorageService implements StorageService {
@override
Future<Set<VolumeRelativeDirectory>> getRestrictedDirectories() async {
try {
final result = await platform.invokeMethod('getRestrictedDirectories');
final result = await _platform.invokeMethod('getRestrictedDirectories');
if (result != null) {
return (result as List).cast<Map>().map(VolumeRelativeDirectory.fromMap).toSet();
}
@ -109,7 +109,7 @@ class PlatformStorageService implements StorageService {
@override
Future<void> revokeDirectoryAccess(String path) async {
try {
await platform.invokeMethod('revokeDirectoryAccess', <String, dynamic>{
await _platform.invokeMethod('revokeDirectoryAccess', <String, dynamic>{
'path': path,
});
} on PlatformException catch (e, stack) {
@ -122,7 +122,7 @@ class PlatformStorageService implements StorageService {
@override
Future<int> deleteEmptyDirectories(Iterable<String> dirPaths) async {
try {
final result = await platform.invokeMethod('deleteEmptyDirectories', <String, dynamic>{
final result = await _platform.invokeMethod('deleteEmptyDirectories', <String, dynamic>{
'dirPaths': dirPaths.toList(),
});
if (result != null) return result as int;
@ -135,7 +135,7 @@ class PlatformStorageService implements StorageService {
@override
Future<bool> canRequestMediaFileAccess() async {
try {
final result = await platform.invokeMethod('canRequestMediaFileBulkAccess');
final result = await _platform.invokeMethod('canRequestMediaFileBulkAccess');
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
@ -146,7 +146,7 @@ class PlatformStorageService implements StorageService {
@override
Future<bool> canInsertMedia(Set<VolumeRelativeDirectory> directories) async {
try {
final result = await platform.invokeMethod('canInsertMedia', <String, dynamic>{
final result = await _platform.invokeMethod('canInsertMedia', <String, dynamic>{
'directories': directories.map((v) => v.toMap()).toList(),
});
if (result != null) return result as bool;
@ -161,7 +161,7 @@ class PlatformStorageService implements StorageService {
Future<bool> requestDirectoryAccess(String path) async {
try {
final completer = Completer<bool>();
storageAccessChannel.receiveBroadcastStream(<String, dynamic>{
_stream.receiveBroadcastStream(<String, dynamic>{
'op': 'requestDirectoryAccess',
'path': path,
}).listen(
@ -185,7 +185,7 @@ class PlatformStorageService implements StorageService {
Future<bool> requestMediaFileAccess(List<String> uris, List<String> mimeTypes) async {
try {
final completer = Completer<bool>();
storageAccessChannel.receiveBroadcastStream(<String, dynamic>{
_stream.receiveBroadcastStream(<String, dynamic>{
'op': 'requestMediaFileAccess',
'uris': uris,
'mimeTypes': mimeTypes,
@ -216,7 +216,7 @@ class PlatformStorageService implements StorageService {
Future<bool?> createFile(String name, String mimeType, Uint8List bytes) async {
try {
final completer = Completer<bool?>();
storageAccessChannel.receiveBroadcastStream(<String, dynamic>{
_stream.receiveBroadcastStream(<String, dynamic>{
'op': 'createFile',
'name': name,
'mimeType': mimeType,
@ -242,7 +242,7 @@ class PlatformStorageService implements StorageService {
try {
final completer = Completer<Uint8List>.sync();
final sink = OutputBuffer();
storageAccessChannel.receiveBroadcastStream(<String, dynamic>{
_stream.receiveBroadcastStream(<String, dynamic>{
'op': 'openFile',
'mimeType': mimeType,
}).listen(

View file

@ -1,27 +0,0 @@
import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart';
class ViewerService {
static const platform = MethodChannel('deckers.thibault/aves/viewer');
static Future<Map<String, dynamic>> getIntentData() async {
try {
// returns nullable map with 'action' and possibly 'uri' 'mimeType'
final result = await platform.invokeMethod('getIntentData');
if (result != null) return (result as Map).cast<String, dynamic>();
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return {};
}
static Future<void> pick(List<String> uris) async {
try {
await platform.invokeMethod('pick', <String, dynamic>{
'uris': uris,
});
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
}
}

View file

@ -5,11 +5,11 @@ import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart';
class WallpaperService {
static const platform = MethodChannel('deckers.thibault/aves/wallpaper');
static const _platform = MethodChannel('deckers.thibault/aves/wallpaper');
static Future<bool> set(Uint8List bytes, WallpaperTarget target) async {
try {
await platform.invokeMethod('setWallpaper', <String, dynamic>{
await _platform.invokeMethod('setWallpaper', <String, dynamic>{
'bytes': bytes,
'home': {WallpaperTarget.home, WallpaperTarget.homeLock}.contains(target),
'lock': {WallpaperTarget.lock, WallpaperTarget.homeLock}.contains(target),

View file

@ -17,12 +17,12 @@ abstract class WindowService {
}
class PlatformWindowService implements WindowService {
static const platform = MethodChannel('deckers.thibault/aves/window');
static const _platform = MethodChannel('deckers.thibault/aves/window');
@override
Future<bool> isActivity() async {
try {
final result = await platform.invokeMethod('isActivity');
final result = await _platform.invokeMethod('isActivity');
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
@ -33,7 +33,7 @@ class PlatformWindowService implements WindowService {
@override
Future<void> keepScreenOn(bool on) async {
try {
await platform.invokeMethod('keepScreenOn', <String, dynamic>{
await _platform.invokeMethod('keepScreenOn', <String, dynamic>{
'on': on,
});
} on PlatformException catch (e, stack) {
@ -44,7 +44,7 @@ class PlatformWindowService implements WindowService {
@override
Future<bool> isRotationLocked() async {
try {
final result = await platform.invokeMethod('isRotationLocked');
final result = await _platform.invokeMethod('isRotationLocked');
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
@ -71,7 +71,7 @@ class PlatformWindowService implements WindowService {
break;
}
try {
await platform.invokeMethod('requestOrientation', <String, dynamic>{
await _platform.invokeMethod('requestOrientation', <String, dynamic>{
'orientation': orientationCode,
});
} on PlatformException catch (e, stack) {
@ -82,7 +82,7 @@ class PlatformWindowService implements WindowService {
@override
Future<bool> canSetCutoutMode() async {
try {
final result = await platform.invokeMethod('canSetCutoutMode');
final result = await _platform.invokeMethod('canSetCutoutMode');
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
@ -93,7 +93,7 @@ class PlatformWindowService implements WindowService {
@override
Future<void> setCutoutMode(bool use) async {
try {
await platform.invokeMethod('setCutoutMode', <String, dynamic>{
await _platform.invokeMethod('setCutoutMode', <String, dynamic>{
'use': use,
});
} on PlatformException catch (e, stack) {

View file

@ -83,7 +83,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
// the list itself needs to be reassigned
List<NavigatorObserver> _navigatorObservers = [AvesApp.pageRouteObserver];
final EventChannel _mediaStoreChangeChannel = const OptionalEventChannel('deckers.thibault/aves/media_store_change');
final EventChannel _newIntentChannel = const OptionalEventChannel('deckers.thibault/aves/intent');
final EventChannel _newIntentChannel = const OptionalEventChannel('deckers.thibault/aves/new_intent_stream');
final EventChannel _analysisCompletionChannel = const OptionalEventChannel('deckers.thibault/aves/analysis_events');
final EventChannel _errorChannel = const OptionalEventChannel('deckers.thibault/aves/error');
@ -228,6 +228,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
case AppMode.pickMultipleMediaExternal:
_saveTopEntries();
break;
case AppMode.pickCollectionFiltersExternal:
case AppMode.pickMediaInternal:
case AppMode.pickFilterInternal:
case AppMode.screenSaver:

View file

@ -138,7 +138,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
return AvesAppBar(
contentHeight: appBarContentHeight,
leading: _buildAppBarLeading(
hasDrawer: appMode.hasDrawer,
hasDrawer: appMode.canNavigate,
isSelecting: isSelecting,
),
title: _buildAppBarTitle(isSelecting),
@ -228,7 +228,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
);
}
return InteractiveAppBarTitle(
onTap: appMode.canSearch ? _goToSearch : null,
onTap: appMode.canNavigate ? _goToSearch : null,
child: title,
);
}

View file

@ -380,8 +380,10 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
selector: (context, mq) => mq.effectiveBottomPadding,
builder: (context, mqPaddingBottom, child) {
return Selector<Settings, bool>(
selector: (context, s) => s.showBottomNavigationBar,
builder: (context, showBottomNavigationBar, child) {
selector: (context, s) => s.enableBottomNavigationBar,
builder: (context, enableBottomNavigationBar, child) {
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
final navBarHeight = showBottomNavigationBar ? AppBottomNavBar.height : 0;
return Selector<SectionedListLayout<AvesEntry>, List<SectionLayout>>(
selector: (context, layout) => layout.sectionLayouts,

View file

@ -10,7 +10,7 @@ import 'package:aves/model/selection.dart';
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/services/viewer_service.dart';
import 'package:aves/services/intent_service.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/collection_grid.dart';
@ -79,7 +79,6 @@ class _CollectionPageState extends State<CollectionPage> {
@override
Widget build(BuildContext context) {
final appMode = context.watch<ValueNotifier<AppMode>>().value;
final liveFilter = _collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?;
return MediaQueryDataProvider(
child: SelectionProvider<AvesEntry>(
@ -87,8 +86,10 @@ class _CollectionPageState extends State<CollectionPage> {
selector: (context, selection) => selection.selectedItems.isNotEmpty,
builder: (context, hasSelection, child) {
return Selector<Settings, bool>(
selector: (context, s) => s.showBottomNavigationBar,
builder: (context, showBottomNavigationBar, child) {
selector: (context, s) => s.enableBottomNavigationBar,
builder: (context, enableBottomNavigationBar, child) {
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
return NotificationListener<DraggableScrollBarNotification>(
onNotification: (notification) {
_draggableScrollBarEventStreamController.add(notification.event);
@ -126,25 +127,7 @@ class _CollectionPageState extends State<CollectionPage> {
),
),
),
floatingActionButton: appMode == AppMode.pickMultipleMediaExternal && hasSelection
? TooltipTheme(
data: TooltipTheme.of(context).copyWith(
preferBelow: false,
),
child: FloatingActionButton(
tooltip: context.l10n.collectionPickPageTitle,
onPressed: () {
final items = context.read<Selection<AvesEntry>>().selectedItems;
final uris = items.map((entry) => entry.uri).toList();
ViewerService.pick(uris);
},
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
child: const Icon(AIcons.apply),
),
)
: null,
floatingActionButton: _buildFab(context, hasSelection),
drawer: AppDrawer(currentCollection: _collection),
bottomNavigationBar: showBottomNavigationBar
? AppBottomNavBar(
@ -164,6 +147,59 @@ class _CollectionPageState extends State<CollectionPage> {
);
}
Widget? _buildFab(BuildContext context, bool hasSelection) {
Widget fab({
required String tooltip,
required VoidCallback onPressed,
}) {
return TooltipTheme(
data: TooltipTheme.of(context).copyWith(
preferBelow: false,
),
child: FloatingActionButton(
tooltip: tooltip,
onPressed: onPressed,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
child: const Icon(AIcons.apply),
),
);
}
final appMode = context.watch<ValueNotifier<AppMode>>().value;
switch (appMode) {
case AppMode.pickMultipleMediaExternal:
return hasSelection
? fab(
tooltip: context.l10n.collectionPickPageTitle,
onPressed: () {
final items = context.read<Selection<AvesEntry>>().selectedItems;
final uris = items.map((entry) => entry.uri).toList();
IntentService.submitPickedItems(uris);
},
)
: null;
case AppMode.pickCollectionFiltersExternal:
return fab(
tooltip: context.l10n.collectionPickPageTitle,
onPressed: () {
final filters = _collection.filters;
IntentService.submitPickedCollectionFilters(filters);
},
);
case AppMode.main:
case AppMode.pickSingleMediaExternal:
case AppMode.pickMediaInternal:
case AppMode.pickFilterInternal:
case AppMode.screenSaver:
case AppMode.setWallpaper:
case AppMode.slideshow:
case AppMode.view:
return null;
}
}
Future<void> _checkInitHighlight() async {
final highlightTest = widget.highlightTest;
if (highlightTest == null) return;

View file

@ -65,7 +65,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
return isSelecting && selectedItemCount == itemCount;
// browsing
case EntrySetAction.searchCollection:
return appMode.canSearch && !isSelecting;
return appMode.canNavigate && !isSelecting;
case EntrySetAction.toggleTitleSearch:
return !isSelecting;
case EntrySetAction.addShortcut:

View file

@ -14,7 +14,7 @@ class FilterBar extends StatefulWidget {
FilterBar({
super.key,
required Set<CollectionFilter> filters,
required this.removable,
this.removable = false,
this.onTap,
}) : filters = List<CollectionFilter>.from(filters)..sort();

View file

@ -3,7 +3,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/services/viewer_service.dart';
import 'package:aves/services/intent_service.dart';
import 'package:aves/widgets/collection/grid/list_details.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/common/behaviour/routes.dart';
@ -44,7 +44,7 @@ class InteractiveTile extends StatelessWidget {
}
break;
case AppMode.pickSingleMediaExternal:
ViewerService.pick([entry.uri]);
IntentService.submitPickedItems([entry.uri]);
break;
case AppMode.pickMultipleMediaExternal:
final selection = context.read<Selection<AvesEntry>>();
@ -53,6 +53,7 @@ class InteractiveTile extends StatelessWidget {
case AppMode.pickMediaInternal:
Navigator.pop(context, entry);
break;
case AppMode.pickCollectionFiltersExternal:
case AppMode.pickFilterInternal:
case AppMode.screenSaver:
case AppMode.setWallpaper:

View file

@ -78,7 +78,7 @@ class AvesFilterChip extends StatefulWidget {
});
static Future<void> showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async {
if (context.read<ValueNotifier<AppMode>>().value == AppMode.main) {
if (context.read<ValueNotifier<AppMode>>().value.canNavigate) {
final actions = [
if (filter is AlbumFilter) ChipAction.goToAlbumPage,
if ((filter is LocationFilter && filter.level == LocationLevel.country)) ChipAction.goToCountryPage,

View file

@ -61,7 +61,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
return isSelecting && selectedItemCount == itemCount;
// browsing
case ChipSetAction.search:
return appMode.canSearch && !isSelecting;
return appMode.canNavigate && !isSelecting;
case ChipSetAction.createAlbum:
return false;
// browsing or selecting

View file

@ -77,7 +77,7 @@ class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGri
return AvesAppBar(
contentHeight: kToolbarHeight,
leading: _buildAppBarLeading(
hasDrawer: appMode.hasDrawer,
hasDrawer: appMode.canNavigate,
isSelecting: isSelecting,
),
title: _buildAppBarTitle(isSelecting),
@ -127,7 +127,7 @@ class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGri
} else {
final appMode = context.watch<ValueNotifier<AppMode>>().value;
return InteractiveAppBarTitle(
onTap: appMode.canSearch ? _goToSearch : null,
onTap: appMode.canNavigate ? _goToSearch : null,
child: SourceStateAwareAppBarTitle(
title: Text(widget.title),
source: source,

View file

@ -74,8 +74,10 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
Widget build(BuildContext context) {
return MediaQueryDataProvider(
child: Selector<Settings, bool>(
selector: (context, s) => s.showBottomNavigationBar,
builder: (context, showBottomNavigationBar, child) {
selector: (context, s) => s.enableBottomNavigationBar,
builder: (context, enableBottomNavigationBar, child) {
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
return NotificationListener<DraggableScrollBarNotification>(
onNotification: (notification) {
_draggableScrollBarEventStreamController.add(notification.event);
@ -524,8 +526,10 @@ class _FilterScrollView<T extends CollectionFilter> extends StatelessWidget {
selector: (context, mq) => mq.effectiveBottomPadding,
builder: (context, mqPaddingBottom, child) {
return Selector<Settings, bool>(
selector: (context, s) => s.showBottomNavigationBar,
builder: (context, showBottomNavigationBar, child) {
selector: (context, s) => s.enableBottomNavigationBar,
builder: (context, enableBottomNavigationBar, child) {
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
final navBarHeight = showBottomNavigationBar ? AppBottomNavBar.height : 0;
return DraggableScrollbar(
backgroundColor: Colors.white,

View file

@ -50,6 +50,7 @@ class _InteractiveFilterTileState<T extends CollectionFilter> extends State<Inte
final appMode = context.read<ValueNotifier<AppMode>>().value;
switch (appMode) {
case AppMode.main:
case AppMode.pickCollectionFiltersExternal:
case AppMode.pickSingleMediaExternal:
case AppMode.pickMultipleMediaExternal:
final selection = context.read<Selection<FilterGridItem<T>>>();

View file

@ -13,7 +13,7 @@ import 'package:aves/model/source/enums.dart';
import 'package:aves/services/analysis_service.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/global_search.dart';
import 'package:aves/services/viewer_service.dart';
import 'package:aves/services/intent_service.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/behaviour/routes.dart';
@ -47,10 +47,11 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> {
AvesEntry? _viewerEntry;
String? _shortcutRouteName, _shortcutSearchQuery;
Set<String>? _shortcutFilters;
String? _initialRouteName, _initialSearchQuery;
Set<CollectionFilter>? _initialFilters;
static const actionPick = 'pick';
static const actionPickItems = 'pick_items';
static const actionPickCollectionFilters = 'pick_collection_filters';
static const actionScreenSaver = 'screen_saver';
static const actionScreenSaverSettings = 'screen_saver_settings';
static const actionSearch = 'search';
@ -86,7 +87,7 @@ class _HomePageState extends State<HomePage> {
}
var appMode = AppMode.main;
final intentData = widget.intentData ?? await ViewerService.getIntentData();
final intentData = widget.intentData ?? await IntentService.getIntentData();
final intentAction = intentData['action'];
if (!{actionScreenSaver, actionSetWallpaper}.contains(intentAction)) {
@ -108,7 +109,7 @@ class _HomePageState extends State<HomePage> {
appMode = AppMode.view;
}
break;
case actionPick:
case actionPickItems:
// TODO TLAD apply pick mimetype(s)
// some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?)
String? pickMimeTypes = intentData['mimeType'];
@ -116,16 +117,19 @@ class _HomePageState extends State<HomePage> {
debugPrint('pick mimeType=$pickMimeTypes multiple=$multiple');
appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal;
break;
case actionPickCollectionFilters:
appMode = AppMode.pickCollectionFiltersExternal;
break;
case actionScreenSaver:
appMode = AppMode.screenSaver;
_shortcutRouteName = ScreenSaverPage.routeName;
_initialRouteName = ScreenSaverPage.routeName;
break;
case actionScreenSaverSettings:
_shortcutRouteName = ScreenSaverSettingsPage.routeName;
_initialRouteName = ScreenSaverSettingsPage.routeName;
break;
case actionSearch:
_shortcutRouteName = CollectionSearchDelegate.pageRouteName;
_shortcutSearchQuery = intentData['query'];
_initialRouteName = CollectionSearchDelegate.pageRouteName;
_initialSearchQuery = intentData['query'];
break;
case actionSetWallpaper:
appMode = AppMode.setWallpaper;
@ -138,11 +142,11 @@ class _HomePageState extends State<HomePage> {
// do not use 'route' as extra key, as the Flutter framework acts on it
final extraRoute = intentData['page'];
if (allowedShortcutRoutes.contains(extraRoute)) {
_shortcutRouteName = extraRoute;
_initialRouteName = extraRoute;
}
final extraFilters = intentData['filters'];
_shortcutFilters = extraFilters != null ? (extraFilters as List).cast<String>().toSet() : null;
}
final extraFilters = intentData['filters'];
_initialFilters = extraFilters != null ? (extraFilters as List).cast<String>().map(CollectionFilter.fromJson).whereNotNull().toSet() : null;
}
context.read<ValueNotifier<AppMode>>().value = appMode;
unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
@ -150,6 +154,7 @@ class _HomePageState extends State<HomePage> {
switch (appMode) {
case AppMode.main:
case AppMode.pickCollectionFiltersExternal:
case AppMode.pickSingleMediaExternal:
case AppMode.pickMultipleMediaExternal:
unawaited(GlobalSearch.registerCallback());
@ -281,8 +286,8 @@ class _HomePageState extends State<HomePage> {
routeName = CollectionPage.routeName;
break;
default:
routeName = _shortcutRouteName ?? settings.homePage.routeName;
filters = (_shortcutFilters ?? {}).map(CollectionFilter.fromJson).toSet();
routeName = _initialRouteName ?? settings.homePage.routeName;
filters = _initialFilters ?? {};
break;
}
final source = context.read<CollectionSource>();
@ -310,7 +315,7 @@ class _HomePageState extends State<HomePage> {
searchFieldLabel: context.l10n.searchCollectionFieldHint,
source: source,
canPop: false,
initialQuery: _shortcutSearchQuery,
initialQuery: _initialSearchQuery,
),
);
case CollectionPage.routeName:

View file

@ -1,3 +1,4 @@
import 'package:aves/app_mode.dart';
import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/settings/settings.dart';
@ -14,6 +15,7 @@ import 'package:aves/widgets/navigation/nav_bar/nav_item.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider/provider.dart';
class AppBottomNavBar extends StatefulWidget {
final Stream<DraggableScrollBarEvent> events;
@ -163,8 +165,10 @@ class NavBarPaddingSliver extends StatelessWidget {
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Selector<Settings, bool>(
selector: (context, s) => s.showBottomNavigationBar,
builder: (context, showBottomNavigationBar, child) {
selector: (context, s) => s.enableBottomNavigationBar,
builder: (context, enableBottomNavigationBar, child) {
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
return SizedBox(height: showBottomNavigationBar ? AppBottomNavBar.height : 0);
},
),

View file

@ -60,8 +60,8 @@ class SettingsTileShowBottomNavigationBar extends SettingsTile {
@override
Widget build(BuildContext context) => SettingsSwitchListTile(
selector: (context, s) => s.showBottomNavigationBar,
onChanged: (v) => settings.showBottomNavigationBar = v,
selector: (context, s) => s.enableBottomNavigationBar,
onChanged: (v) => settings.enableBottomNavigationBar = v,
title: title(context),
);
}

View file

@ -1,11 +1,16 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/slideshow_interval.dart';
import 'package:aves/model/settings/enums/slideshow_video_playback.dart';
import 'package:aves/model/settings/enums/viewer_transition.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/intent_service.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/filter_bar.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/common/tiles.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ScreenSaverSettingsPage extends StatelessWidget {
static const routeName = '/settings/screen_saver';
@ -14,9 +19,10 @@ class ScreenSaverSettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.settingsScreenSaverTitle),
title: Text(l10n.settingsScreenSaverTitle),
),
body: SafeArea(
child: ListView(
@ -26,24 +32,77 @@ class ScreenSaverSettingsPage extends StatelessWidget {
getName: (context, v) => v.getName(context),
selector: (context, s) => s.screenSaverTransition,
onSelection: (v) => settings.screenSaverTransition = v,
tileTitle: context.l10n.settingsSlideshowTransitionTile,
dialogTitle: context.l10n.settingsSlideshowTransitionTitle,
tileTitle: l10n.settingsSlideshowTransitionTile,
dialogTitle: l10n.settingsSlideshowTransitionTitle,
),
SettingsSelectionListTile<SlideshowInterval>(
values: SlideshowInterval.values,
getName: (context, v) => v.getName(context),
selector: (context, s) => s.screenSaverInterval,
onSelection: (v) => settings.screenSaverInterval = v,
tileTitle: context.l10n.settingsSlideshowIntervalTile,
dialogTitle: context.l10n.settingsSlideshowIntervalTitle,
tileTitle: l10n.settingsSlideshowIntervalTile,
dialogTitle: l10n.settingsSlideshowIntervalTitle,
),
SettingsSelectionListTile<SlideshowVideoPlayback>(
values: SlideshowVideoPlayback.values,
getName: (context, v) => v.getName(context),
selector: (context, s) => s.screenSaverVideoPlayback,
onSelection: (v) => settings.screenSaverVideoPlayback = v,
tileTitle: context.l10n.settingsSlideshowVideoPlaybackTile,
dialogTitle: context.l10n.settingsSlideshowVideoPlaybackTitle,
tileTitle: l10n.settingsSlideshowVideoPlaybackTile,
dialogTitle: l10n.settingsSlideshowVideoPlaybackTitle,
),
Selector<Settings, Set<CollectionFilter>>(
selector: (context, s) => s.screenSaverCollectionFilters,
builder: (context, filters, child) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final hasSubtitle = filters.isEmpty;
// size and padding to match `ListTile`
return ConstrainedBox(
constraints: BoxConstraints(
minHeight: (hasSubtitle ? 72.0 : 56.0) + theme.visualDensity.baseSizeAdjustment.dy,
),
child: Center(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.collectionPageTitle,
style: textTheme.subtitle1!,
),
if (hasSubtitle)
Text(
l10n.drawerCollectionAll,
style: textTheme.bodyText2!.copyWith(color: textTheme.caption!.color),
),
],
),
const Spacer(),
IconButton(
onPressed: () async {
final selection = await IntentService.pickCollectionFilters(filters);
if (selection != null) {
settings.screenSaverCollectionFilters = selection;
}
},
icon: const Icon(AIcons.edit),
),
],
),
),
if (filters.isNotEmpty) FilterBar(filters: filters),
],
),
),
);
},
),
],
),

View file

@ -105,7 +105,7 @@ class _ScreenSaverPageState extends State<ScreenSaverPage> {
final originalCollection = CollectionLens(
source: source,
// TODO TLAD [screensaver] custom filters
filters: settings.screenSaverCollectionFilters,
);
var entries = originalCollection.sortedEntries;
if (settings.screenSaverVideoPlayback == SlideshowVideoPlayback.skip) {

View file

@ -28,7 +28,7 @@ Future<void> configureAndLaunch() async {
// navigation
..keepScreenOn = KeepScreenOn.always
..homePage = HomePageSetting.collection
..showBottomNavigationBar = true
..enableBottomNavigationBar = true
// collection
..collectionSectionFactor = EntryGroupFactor.month
..collectionSortFactor = EntrySortFactor.date

View file

@ -27,7 +27,7 @@ Future<void> configureAndLaunch() async {
// navigation
..keepScreenOn = KeepScreenOn.always
..homePage = HomePageSetting.collection
..showBottomNavigationBar = true
..enableBottomNavigationBar = true
// collection
..collectionBrowsingQuickActions = SettingsDefaults.collectionBrowsingQuickActions
// viewer