#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 - Search: `on this day` filter
- Stats: histogram and date filters - Stats: histogram and date filters
- Screen saver
### Changed ### Changed

View file

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

View file

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

View file

@ -49,7 +49,7 @@ class WallpaperActivity : FlutterActivity() {
// intent handling // intent handling
// detail fetch: dart -> platform // detail fetch: dart -> platform
intentDataMap = extractIntentData(intent) intentDataMap = extractIntentData(intent)
MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result -> MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"getIntentData" -> { "getIntentData" -> {
result.success(intentDataMap) 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?> { private fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
when (intent?.action) { when (intent?.action) {
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> { Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
@ -108,6 +98,5 @@ class WallpaperActivity : FlutterActivity() {
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<WallpaperActivity>() 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 com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.MainActivity import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.MainActivity.Companion.EXTRA_STRING_ARRAY_SEPARATOR 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.EXTRA_KEY_FILTERS_ARRAY
import deckers.thibault.aves.MainActivity.Companion.SHORTCUT_KEY_FILTERS_STRING import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING
import deckers.thibault.aves.MainActivity.Companion.SHORTCUT_KEY_PAGE import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE
import deckers.thibault.aves.R import deckers.thibault.aves.R
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
@ -407,11 +407,11 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val intent = when { val intent = when {
uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java) uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java)
filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java) filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(SHORTCUT_KEY_PAGE, "/collection") .putExtra(EXTRA_KEY_PAGE, "/collection")
.putExtra(SHORTCUT_KEY_FILTERS_ARRAY, filters.toTypedArray()) .putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray())
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut // on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
// so we use a joined `String` as fallback // 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 -> { else -> {
result.error("pin-intent", "failed to build intent", null) result.error("pin-intent", "failed to build intent", null)
return return

View file

@ -22,9 +22,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch 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 // 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 val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private lateinit var eventSink: EventSink private lateinit var eventSink: EventSink
private lateinit var handler: Handler private lateinit var handler: Handler
@ -48,6 +48,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
"requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() } "requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() }
"createFile" -> ioScope.launch { createFile() } "createFile" -> ioScope.launch { createFile() }
"openFile" -> ioScope.launch { openFile() } "openFile" -> ioScope.launch { openFile() }
"pickCollectionFilters" -> pickCollectionFilters()
else -> endOfStream() 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?) {} override fun onCancel(arguments: Any?) {}
private fun success(result: Any?) { private fun success(result: Any?) {
@ -221,8 +234,8 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
} }
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<StorageAccessStreamHandler>() private val LOG_TAG = LogUtils.createTag<ActivityResultStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/storage_access_stream" const val CHANNEL = "deckers.thibault/aves/activity_result_stream"
private const val BUFFER_SIZE = 2 shl 17 // 256kB private const val BUFFER_SIZE = 2 shl 17 // 256kB
} }
} }

View file

@ -20,6 +20,6 @@ class IntentStreamHandler : EventChannel.StreamHandler {
} }
companion object { 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 { enum AppMode {
main, main,
pickCollectionFiltersExternal,
pickSingleMediaExternal, pickSingleMediaExternal,
pickMultipleMediaExternal, pickMultipleMediaExternal,
pickMediaInternal, pickMediaInternal,
@ -11,13 +12,23 @@ enum AppMode {
} }
extension ExtraAppMode on 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 canSelectFilter => this == AppMode.main;
bool get hasDrawer => this == AppMode.main || this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal; bool get isPickingMedia => {
AppMode.pickSingleMediaExternal,
bool get isPickingMedia => this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal || this == AppMode.pickMediaInternal; AppMode.pickMultipleMediaExternal,
AppMode.pickMediaInternal,
}.contains(this);
} }

View file

@ -31,7 +31,7 @@ class SettingsDefaults {
static const mustBackTwiceToExit = true; static const mustBackTwiceToExit = true;
static const keepScreenOn = KeepScreenOn.viewerOnly; static const keepScreenOn = KeepScreenOn.viewerOnly;
static const homePage = HomePageSetting.collection; static const homePage = HomePageSetting.collection;
static const showBottomNavigationBar = true; static const enableBottomNavigationBar = true;
static const confirmDeleteForever = true; static const confirmDeleteForever = true;
static const confirmMoveToBin = true; static const confirmMoveToBin = true;
static const confirmMoveUndatedItems = 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 mustBackTwiceToExitKey = 'must_back_twice_to_exit';
static const keepScreenOnKey = 'keep_screen_on'; static const keepScreenOnKey = 'keep_screen_on';
static const homePageKey = 'home_page'; 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 confirmDeleteForeverKey = 'confirm_delete_forever';
static const confirmMoveToBinKey = 'confirm_move_to_bin'; static const confirmMoveToBinKey = 'confirm_move_to_bin';
static const confirmMoveUndatedItemsKey = 'confirm_move_undated_items'; static const confirmMoveUndatedItemsKey = 'confirm_move_undated_items';
@ -142,6 +142,7 @@ class Settings extends ChangeNotifier {
static const screenSaverTransitionKey = 'screen_saver_transition'; static const screenSaverTransitionKey = 'screen_saver_transition';
static const screenSaverVideoPlaybackKey = 'screen_saver_video_playback'; static const screenSaverVideoPlaybackKey = 'screen_saver_video_playback';
static const screenSaverIntervalKey = 'screen_saver_interval'; static const screenSaverIntervalKey = 'screen_saver_interval';
static const screenSaverCollectionFiltersKey = 'screen_saver_collection_filters';
// slideshow // slideshow
static const slideshowRepeatKey = 'slideshow_loop'; static const slideshowRepeatKey = 'slideshow_loop';
@ -320,9 +321,9 @@ class Settings extends ChangeNotifier {
set homePage(HomePageSetting newValue) => setAndNotify(homePageKey, newValue.toString()); 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); bool get confirmDeleteForever => getBoolOrDefault(confirmDeleteForeverKey, SettingsDefaults.confirmDeleteForever);
@ -602,6 +603,10 @@ class Settings extends ChangeNotifier {
set screenSaverInterval(SlideshowInterval newValue) => setAndNotify(screenSaverIntervalKey, newValue.toString()); 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 // slideshow
bool get slideshowRepeat => getBoolOrDefault(slideshowRepeatKey, SettingsDefaults.slideshowRepeat); bool get slideshowRepeat => getBoolOrDefault(slideshowRepeatKey, SettingsDefaults.slideshowRepeat);
@ -754,7 +759,7 @@ class Settings extends ChangeNotifier {
case isErrorReportingAllowedKey: case isErrorReportingAllowedKey:
case enableDynamicColorKey: case enableDynamicColorKey:
case enableBlurEffectKey: case enableBlurEffectKey:
case showBottomNavigationBarKey: case enableBottomNavigationBarKey:
case mustBackTwiceToExitKey: case mustBackTwiceToExitKey:
case confirmDeleteForeverKey: case confirmDeleteForeverKey:
case confirmMoveToBinKey: case confirmMoveToBinKey:
@ -831,6 +836,7 @@ class Settings extends ChangeNotifier {
case collectionBrowsingQuickActionsKey: case collectionBrowsingQuickActionsKey:
case collectionSelectionQuickActionsKey: case collectionSelectionQuickActionsKey:
case viewerQuickActionsKey: case viewerQuickActionsKey:
case screenSaverCollectionFiltersKey:
if (newValue is List) { if (newValue is List) {
settingsStore.setStringList(key, newValue.cast<String>()); settingsStore.setStringList(key, newValue.cast<String>());
} else { } else {

View file

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

View file

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

View file

@ -34,9 +34,9 @@ abstract class AndroidAppService {
} }
class PlatformAndroidAppService implements 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.kakao.talk': {'KakaoTalkDownload'},
'com.sony.playmemories.mobile': {'Imaging Edge Mobile'}, 'com.sony.playmemories.mobile': {'Imaging Edge Mobile'},
'nekox.messenger': {'NekoX'}, 'nekox.messenger': {'NekoX'},
@ -45,10 +45,10 @@ class PlatformAndroidAppService implements AndroidAppService {
@override @override
Future<Set<Package>> getPackages() async { Future<Set<Package>> getPackages() async {
try { try {
final result = await platform.invokeMethod('getPackages'); final result = await _platform.invokeMethod('getPackages');
final packages = (result as List).cast<Map>().map(Package.fromMap).toSet(); final packages = (result as List).cast<Map>().map(Package.fromMap).toSet();
// additional info for known directories // additional info for known directories
knownAppDirs.forEach((packageName, dirs) { _knownAppDirs.forEach((packageName, dirs) {
final package = packages.firstWhereOrNull((package) => package.packageName == packageName); final package = packages.firstWhereOrNull((package) => package.packageName == packageName);
if (package != null) { if (package != null) {
package.ownedDirs.addAll(dirs); package.ownedDirs.addAll(dirs);
@ -64,7 +64,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override @override
Future<Uint8List> getAppIcon(String packageName, double size) async { Future<Uint8List> getAppIcon(String packageName, double size) async {
try { try {
final result = await platform.invokeMethod('getAppIcon', <String, dynamic>{ final result = await _platform.invokeMethod('getAppIcon', <String, dynamic>{
'packageName': packageName, 'packageName': packageName,
'sizeDip': size, 'sizeDip': size,
}); });
@ -78,7 +78,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override @override
Future<String?> getAppInstaller() async { Future<String?> getAppInstaller() async {
try { try {
return await platform.invokeMethod('getAppInstaller'); return await _platform.invokeMethod('getAppInstaller');
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
@ -88,7 +88,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override @override
Future<bool> copyToClipboard(String uri, String? label) async { Future<bool> copyToClipboard(String uri, String? label) async {
try { try {
final result = await platform.invokeMethod('copyToClipboard', <String, dynamic>{ final result = await _platform.invokeMethod('copyToClipboard', <String, dynamic>{
'uri': uri, 'uri': uri,
'label': label, 'label': label,
}); });
@ -102,7 +102,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override @override
Future<bool> edit(String uri, String mimeType) async { Future<bool> edit(String uri, String mimeType) async {
try { try {
final result = await platform.invokeMethod('edit', <String, dynamic>{ final result = await _platform.invokeMethod('edit', <String, dynamic>{
'uri': uri, 'uri': uri,
'mimeType': mimeType, 'mimeType': mimeType,
}); });
@ -116,7 +116,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override @override
Future<bool> open(String uri, String mimeType) async { Future<bool> open(String uri, String mimeType) async {
try { try {
final result = await platform.invokeMethod('open', <String, dynamic>{ final result = await _platform.invokeMethod('open', <String, dynamic>{
'uri': uri, 'uri': uri,
'mimeType': mimeType, 'mimeType': mimeType,
}); });
@ -134,7 +134,7 @@ class PlatformAndroidAppService implements AndroidAppService {
final geoUri = 'geo:$latitude,$longitude?q=$latitude,$longitude'; final geoUri = 'geo:$latitude,$longitude?q=$latitude,$longitude';
try { try {
final result = await platform.invokeMethod('openMap', <String, dynamic>{ final result = await _platform.invokeMethod('openMap', <String, dynamic>{
'geoUri': geoUri, 'geoUri': geoUri,
}); });
if (result != null) return result as bool; if (result != null) return result as bool;
@ -147,7 +147,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override @override
Future<bool> setAs(String uri, String mimeType) async { Future<bool> setAs(String uri, String mimeType) async {
try { try {
final result = await platform.invokeMethod('setAs', <String, dynamic>{ final result = await _platform.invokeMethod('setAs', <String, dynamic>{
'uri': uri, 'uri': uri,
'mimeType': mimeType, '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 // 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())); final urisByMimeType = groupBy<AvesEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
try { try {
final result = await platform.invokeMethod('share', <String, dynamic>{ final result = await _platform.invokeMethod('share', <String, dynamic>{
'urisByMimeType': urisByMimeType, 'urisByMimeType': urisByMimeType,
}); });
if (result != null) return result as bool; if (result != null) return result as bool;
@ -177,7 +177,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override @override
Future<bool> shareSingle(String uri, String mimeType) async { Future<bool> shareSingle(String uri, String mimeType) async {
try { try {
final result = await platform.invokeMethod('share', <String, dynamic>{ final result = await _platform.invokeMethod('share', <String, dynamic>{
'urisByMimeType': { 'urisByMimeType': {
mimeType: [uri] mimeType: [uri]
}, },
@ -207,7 +207,7 @@ class PlatformAndroidAppService implements AndroidAppService {
); );
} }
try { try {
await platform.invokeMethod('pinShortcut', <String, dynamic>{ await _platform.invokeMethod('pinShortcut', <String, dynamic>{
'label': label, 'label': label,
'iconBytes': iconBytes, 'iconBytes': iconBytes,
'filters': filters?.map((filter) => filter.toJson()).toList(), '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'; import 'package:flutter/services.dart';
class AndroidDebugService { class AndroidDebugService {
static const platform = MethodChannel('deckers.thibault/aves/debug'); static const _platform = MethodChannel('deckers.thibault/aves/debug');
static Future<void> crash() async { static Future<void> crash() async {
try { try {
await platform.invokeMethod('crash'); await _platform.invokeMethod('crash');
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
@ -16,7 +16,7 @@ class AndroidDebugService {
static Future<void> exception() async { static Future<void> exception() async {
try { try {
await platform.invokeMethod('exception'); await _platform.invokeMethod('exception');
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
@ -24,7 +24,7 @@ class AndroidDebugService {
static Future<void> safeException() async { static Future<void> safeException() async {
try { try {
await platform.invokeMethod('safeException'); await _platform.invokeMethod('safeException');
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
@ -32,7 +32,7 @@ class AndroidDebugService {
static Future<void> exceptionInCoroutine() async { static Future<void> exceptionInCoroutine() async {
try { try {
await platform.invokeMethod('exceptionInCoroutine'); await _platform.invokeMethod('exceptionInCoroutine');
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
@ -40,7 +40,7 @@ class AndroidDebugService {
static Future<void> safeExceptionInCoroutine() async { static Future<void> safeExceptionInCoroutine() async {
try { try {
await platform.invokeMethod('safeExceptionInCoroutine'); await _platform.invokeMethod('safeExceptionInCoroutine');
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
@ -48,7 +48,7 @@ class AndroidDebugService {
static Future<Map> getContextDirs() async { static Future<Map> getContextDirs() async {
try { try {
final result = await platform.invokeMethod('getContextDirs'); final result = await _platform.invokeMethod('getContextDirs');
if (result != null) return result as Map; if (result != null) return result as Map;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
@ -58,7 +58,7 @@ class AndroidDebugService {
static Future<List<Map>> getCodecs() async { static Future<List<Map>> getCodecs() async {
try { try {
final result = await platform.invokeMethod('getCodecs'); final result = await _platform.invokeMethod('getCodecs');
if (result != null) return (result as List).cast<Map>(); if (result != null) return (result as List).cast<Map>();
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
@ -68,7 +68,7 @@ class AndroidDebugService {
static Future<Map> getEnv() async { static Future<Map> getEnv() async {
try { try {
final result = await platform.invokeMethod('getEnv'); final result = await _platform.invokeMethod('getEnv');
if (result != null) return result as Map; if (result != null) return result as Map;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
@ -79,7 +79,7 @@ class AndroidDebugService {
static Future<Map> getBitmapFactoryInfo(AvesEntry entry) async { static Future<Map> getBitmapFactoryInfo(AvesEntry entry) async {
try { try {
// returns map with all data available when decoding image bounds with `BitmapFactory` // 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, 'uri': entry.uri,
}); });
if (result != null) return result as Map; if (result != null) return result as Map;
@ -92,7 +92,7 @@ class AndroidDebugService {
static Future<Map> getContentResolverMetadata(AvesEntry entry) async { static Future<Map> getContentResolverMetadata(AvesEntry entry) async {
try { try {
// returns map with all data available from the content resolver // 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, 'mimeType': entry.mimeType,
'uri': entry.uri, 'uri': entry.uri,
}); });
@ -106,7 +106,7 @@ class AndroidDebugService {
static Future<Map> getExifInterfaceMetadata(AvesEntry entry) async { static Future<Map> getExifInterfaceMetadata(AvesEntry entry) async {
try { try {
// returns map with all data available from the `ExifInterface` library // 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, 'mimeType': entry.mimeType,
'uri': entry.uri, 'uri': entry.uri,
'sizeBytes': entry.sizeBytes, 'sizeBytes': entry.sizeBytes,
@ -121,7 +121,7 @@ class AndroidDebugService {
static Future<Map> getMediaMetadataRetrieverMetadata(AvesEntry entry) async { static Future<Map> getMediaMetadataRetrieverMetadata(AvesEntry entry) async {
try { try {
// returns map with all data available from `MediaMetadataRetriever` // 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, 'uri': entry.uri,
}); });
if (result != null) return result as Map; if (result != null) return result as Map;
@ -134,7 +134,7 @@ class AndroidDebugService {
static Future<Map> getMetadataExtractorSummary(AvesEntry entry) async { static Future<Map> getMetadataExtractorSummary(AvesEntry entry) async {
try { try {
// returns map with the MIME type and tag count for each directory found by `metadata-extractor` // 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, 'mimeType': entry.mimeType,
'uri': entry.uri, 'uri': entry.uri,
'sizeBytes': entry.sizeBytes, 'sizeBytes': entry.sizeBytes,
@ -149,7 +149,7 @@ class AndroidDebugService {
static Future<Map> getPixyMetadata(AvesEntry entry) async { static Future<Map> getPixyMetadata(AvesEntry entry) async {
try { try {
// returns map with all data available from the `PixyMeta` library // 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, 'mimeType': entry.mimeType,
'uri': entry.uri, 'uri': entry.uri,
}); });
@ -164,7 +164,7 @@ class AndroidDebugService {
if (entry.mimeType != MimeTypes.tiff) return {}; if (entry.mimeType != MimeTypes.tiff) return {};
try { try {
final result = await platform.invokeMethod('getTiffStructure', <String, dynamic>{ final result = await _platform.invokeMethod('getTiffStructure', <String, dynamic>{
'uri': entry.uri, 'uri': entry.uri,
}); });
if (result != null) return result as Map; if (result != null) return result as Map;

View file

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

View file

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

View file

@ -7,11 +7,11 @@ import 'package:flutter/widgets.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
class GlobalSearch { 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 { static Future<void> registerCallback() async {
try { try {
await platform.invokeMethod('registerCallback', <String, dynamic>{ await _platform.invokeMethod('registerCallback', <String, dynamic>{
'callbackHandle': PluginUtilities.getCallbackHandle(_init)?.toRawHandle(), 'callbackHandle': PluginUtilities.getCallbackHandle(_init)?.toRawHandle(),
}); });
} on PlatformException catch (e, stack) { } 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 { class PlatformEmbeddedDataService implements EmbeddedDataService {
static const platform = MethodChannel('deckers.thibault/aves/embedded'); static const _platform = MethodChannel('deckers.thibault/aves/embedded');
@override @override
Future<List<Uint8List>> getExifThumbnails(AvesEntry entry) async { Future<List<Uint8List>> getExifThumbnails(AvesEntry entry) async {
try { try {
final result = await platform.invokeMethod('getExifThumbnails', <String, dynamic>{ final result = await _platform.invokeMethod('getExifThumbnails', <String, dynamic>{
'mimeType': entry.mimeType, 'mimeType': entry.mimeType,
'uri': entry.uri, 'uri': entry.uri,
'sizeBytes': entry.sizeBytes, 'sizeBytes': entry.sizeBytes,
@ -35,7 +35,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
@override @override
Future<Map> extractMotionPhotoVideo(AvesEntry entry) async { Future<Map> extractMotionPhotoVideo(AvesEntry entry) async {
try { try {
final result = await platform.invokeMethod('extractMotionPhotoVideo', <String, dynamic>{ final result = await _platform.invokeMethod('extractMotionPhotoVideo', <String, dynamic>{
'mimeType': entry.mimeType, 'mimeType': entry.mimeType,
'uri': entry.uri, 'uri': entry.uri,
'sizeBytes': entry.sizeBytes, 'sizeBytes': entry.sizeBytes,
@ -51,7 +51,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
@override @override
Future<Map> extractVideoEmbeddedPicture(AvesEntry entry) async { Future<Map> extractVideoEmbeddedPicture(AvesEntry entry) async {
try { try {
final result = await platform.invokeMethod('extractVideoEmbeddedPicture', <String, dynamic>{ final result = await _platform.invokeMethod('extractVideoEmbeddedPicture', <String, dynamic>{
'uri': entry.uri, 'uri': entry.uri,
'displayName': '${entry.bestTitle} • Cover', 'displayName': '${entry.bestTitle} • Cover',
}); });
@ -65,7 +65,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
@override @override
Future<Map> extractXmpDataProp(AvesEntry entry, String? propPath, String? propMimeType) async { Future<Map> extractXmpDataProp(AvesEntry entry, String? propPath, String? propMimeType) async {
try { try {
final result = await platform.invokeMethod('extractXmpDataProp', <String, dynamic>{ final result = await _platform.invokeMethod('extractXmpDataProp', <String, dynamic>{
'mimeType': entry.mimeType, 'mimeType': entry.mimeType,
'uri': entry.uri, 'uri': entry.uri,
'sizeBytes': entry.sizeBytes, 'sizeBytes': entry.sizeBytes,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,12 +17,12 @@ abstract class WindowService {
} }
class PlatformWindowService implements WindowService { class PlatformWindowService implements WindowService {
static const platform = MethodChannel('deckers.thibault/aves/window'); static const _platform = MethodChannel('deckers.thibault/aves/window');
@override @override
Future<bool> isActivity() async { Future<bool> isActivity() async {
try { try {
final result = await platform.invokeMethod('isActivity'); final result = await _platform.invokeMethod('isActivity');
if (result != null) return result as bool; if (result != null) return result as bool;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
@ -33,7 +33,7 @@ class PlatformWindowService implements WindowService {
@override @override
Future<void> keepScreenOn(bool on) async { Future<void> keepScreenOn(bool on) async {
try { try {
await platform.invokeMethod('keepScreenOn', <String, dynamic>{ await _platform.invokeMethod('keepScreenOn', <String, dynamic>{
'on': on, 'on': on,
}); });
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
@ -44,7 +44,7 @@ class PlatformWindowService implements WindowService {
@override @override
Future<bool> isRotationLocked() async { Future<bool> isRotationLocked() async {
try { try {
final result = await platform.invokeMethod('isRotationLocked'); final result = await _platform.invokeMethod('isRotationLocked');
if (result != null) return result as bool; if (result != null) return result as bool;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
@ -71,7 +71,7 @@ class PlatformWindowService implements WindowService {
break; break;
} }
try { try {
await platform.invokeMethod('requestOrientation', <String, dynamic>{ await _platform.invokeMethod('requestOrientation', <String, dynamic>{
'orientation': orientationCode, 'orientation': orientationCode,
}); });
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
@ -82,7 +82,7 @@ class PlatformWindowService implements WindowService {
@override @override
Future<bool> canSetCutoutMode() async { Future<bool> canSetCutoutMode() async {
try { try {
final result = await platform.invokeMethod('canSetCutoutMode'); final result = await _platform.invokeMethod('canSetCutoutMode');
if (result != null) return result as bool; if (result != null) return result as bool;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
@ -93,7 +93,7 @@ class PlatformWindowService implements WindowService {
@override @override
Future<void> setCutoutMode(bool use) async { Future<void> setCutoutMode(bool use) async {
try { try {
await platform.invokeMethod('setCutoutMode', <String, dynamic>{ await _platform.invokeMethod('setCutoutMode', <String, dynamic>{
'use': use, 'use': use,
}); });
} on PlatformException catch (e, stack) { } 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 // the list itself needs to be reassigned
List<NavigatorObserver> _navigatorObservers = [AvesApp.pageRouteObserver]; List<NavigatorObserver> _navigatorObservers = [AvesApp.pageRouteObserver];
final EventChannel _mediaStoreChangeChannel = const OptionalEventChannel('deckers.thibault/aves/media_store_change'); 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 _analysisCompletionChannel = const OptionalEventChannel('deckers.thibault/aves/analysis_events');
final EventChannel _errorChannel = const OptionalEventChannel('deckers.thibault/aves/error'); final EventChannel _errorChannel = const OptionalEventChannel('deckers.thibault/aves/error');
@ -228,6 +228,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
case AppMode.pickMultipleMediaExternal: case AppMode.pickMultipleMediaExternal:
_saveTopEntries(); _saveTopEntries();
break; break;
case AppMode.pickCollectionFiltersExternal:
case AppMode.pickMediaInternal: case AppMode.pickMediaInternal:
case AppMode.pickFilterInternal: case AppMode.pickFilterInternal:
case AppMode.screenSaver: case AppMode.screenSaver:

View file

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

View file

@ -380,8 +380,10 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
selector: (context, mq) => mq.effectiveBottomPadding, selector: (context, mq) => mq.effectiveBottomPadding,
builder: (context, mqPaddingBottom, child) { builder: (context, mqPaddingBottom, child) {
return Selector<Settings, bool>( return Selector<Settings, bool>(
selector: (context, s) => s.showBottomNavigationBar, selector: (context, s) => s.enableBottomNavigationBar,
builder: (context, showBottomNavigationBar, child) { 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; final navBarHeight = showBottomNavigationBar ? AppBottomNavBar.height : 0;
return Selector<SectionedListLayout<AvesEntry>, List<SectionLayout>>( return Selector<SectionedListLayout<AvesEntry>, List<SectionLayout>>(
selector: (context, layout) => layout.sectionLayouts, 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/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/viewer_service.dart'; import 'package:aves/services/intent_service.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/collection/collection_grid.dart';
@ -79,7 +79,6 @@ class _CollectionPageState extends State<CollectionPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appMode = context.watch<ValueNotifier<AppMode>>().value;
final liveFilter = _collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?; final liveFilter = _collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?;
return MediaQueryDataProvider( return MediaQueryDataProvider(
child: SelectionProvider<AvesEntry>( child: SelectionProvider<AvesEntry>(
@ -87,8 +86,10 @@ class _CollectionPageState extends State<CollectionPage> {
selector: (context, selection) => selection.selectedItems.isNotEmpty, selector: (context, selection) => selection.selectedItems.isNotEmpty,
builder: (context, hasSelection, child) { builder: (context, hasSelection, child) {
return Selector<Settings, bool>( return Selector<Settings, bool>(
selector: (context, s) => s.showBottomNavigationBar, selector: (context, s) => s.enableBottomNavigationBar,
builder: (context, showBottomNavigationBar, child) { builder: (context, enableBottomNavigationBar, child) {
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
final showBottomNavigationBar = canNavigate && enableBottomNavigationBar;
return NotificationListener<DraggableScrollBarNotification>( return NotificationListener<DraggableScrollBarNotification>(
onNotification: (notification) { onNotification: (notification) {
_draggableScrollBarEventStreamController.add(notification.event); _draggableScrollBarEventStreamController.add(notification.event);
@ -126,25 +127,7 @@ class _CollectionPageState extends State<CollectionPage> {
), ),
), ),
), ),
floatingActionButton: appMode == AppMode.pickMultipleMediaExternal && hasSelection floatingActionButton: _buildFab(context, 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,
drawer: AppDrawer(currentCollection: _collection), drawer: AppDrawer(currentCollection: _collection),
bottomNavigationBar: showBottomNavigationBar bottomNavigationBar: showBottomNavigationBar
? AppBottomNavBar( ? 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 { Future<void> _checkInitHighlight() async {
final highlightTest = widget.highlightTest; final highlightTest = widget.highlightTest;
if (highlightTest == null) return; if (highlightTest == null) return;

View file

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

View file

@ -14,7 +14,7 @@ class FilterBar extends StatefulWidget {
FilterBar({ FilterBar({
super.key, super.key,
required Set<CollectionFilter> filters, required Set<CollectionFilter> filters,
required this.removable, this.removable = false,
this.onTap, this.onTap,
}) : filters = List<CollectionFilter>.from(filters)..sort(); }) : 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/selection.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/enums.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.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart'; import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/behaviour/routes.dart';
@ -44,7 +44,7 @@ class InteractiveTile extends StatelessWidget {
} }
break; break;
case AppMode.pickSingleMediaExternal: case AppMode.pickSingleMediaExternal:
ViewerService.pick([entry.uri]); IntentService.submitPickedItems([entry.uri]);
break; break;
case AppMode.pickMultipleMediaExternal: case AppMode.pickMultipleMediaExternal:
final selection = context.read<Selection<AvesEntry>>(); final selection = context.read<Selection<AvesEntry>>();
@ -53,6 +53,7 @@ class InteractiveTile extends StatelessWidget {
case AppMode.pickMediaInternal: case AppMode.pickMediaInternal:
Navigator.pop(context, entry); Navigator.pop(context, entry);
break; break;
case AppMode.pickCollectionFiltersExternal:
case AppMode.pickFilterInternal: case AppMode.pickFilterInternal:
case AppMode.screenSaver: case AppMode.screenSaver:
case AppMode.setWallpaper: case AppMode.setWallpaper:

View file

@ -78,7 +78,7 @@ class AvesFilterChip extends StatefulWidget {
}); });
static Future<void> showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async { 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 = [ final actions = [
if (filter is AlbumFilter) ChipAction.goToAlbumPage, if (filter is AlbumFilter) ChipAction.goToAlbumPage,
if ((filter is LocationFilter && filter.level == LocationLevel.country)) ChipAction.goToCountryPage, 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; return isSelecting && selectedItemCount == itemCount;
// browsing // browsing
case ChipSetAction.search: case ChipSetAction.search:
return appMode.canSearch && !isSelecting; return appMode.canNavigate && !isSelecting;
case ChipSetAction.createAlbum: case ChipSetAction.createAlbum:
return false; return false;
// browsing or selecting // browsing or selecting

View file

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

View file

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

View file

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

View file

@ -60,8 +60,8 @@ class SettingsTileShowBottomNavigationBar extends SettingsTile {
@override @override
Widget build(BuildContext context) => SettingsSwitchListTile( Widget build(BuildContext context) => SettingsSwitchListTile(
selector: (context, s) => s.showBottomNavigationBar, selector: (context, s) => s.enableBottomNavigationBar,
onChanged: (v) => settings.showBottomNavigationBar = v, onChanged: (v) => settings.enableBottomNavigationBar = v,
title: title(context), 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/enums.dart';
import 'package:aves/model/settings/enums/slideshow_interval.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/slideshow_video_playback.dart';
import 'package:aves/model/settings/enums/viewer_transition.dart'; import 'package:aves/model/settings/enums/viewer_transition.dart';
import 'package:aves/model/settings/settings.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/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/common/tiles.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ScreenSaverSettingsPage extends StatelessWidget { class ScreenSaverSettingsPage extends StatelessWidget {
static const routeName = '/settings/screen_saver'; static const routeName = '/settings/screen_saver';
@ -14,9 +19,10 @@ class ScreenSaverSettingsPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.l10n.settingsScreenSaverTitle), title: Text(l10n.settingsScreenSaverTitle),
), ),
body: SafeArea( body: SafeArea(
child: ListView( child: ListView(
@ -26,24 +32,77 @@ class ScreenSaverSettingsPage extends StatelessWidget {
getName: (context, v) => v.getName(context), getName: (context, v) => v.getName(context),
selector: (context, s) => s.screenSaverTransition, selector: (context, s) => s.screenSaverTransition,
onSelection: (v) => settings.screenSaverTransition = v, onSelection: (v) => settings.screenSaverTransition = v,
tileTitle: context.l10n.settingsSlideshowTransitionTile, tileTitle: l10n.settingsSlideshowTransitionTile,
dialogTitle: context.l10n.settingsSlideshowTransitionTitle, dialogTitle: l10n.settingsSlideshowTransitionTitle,
), ),
SettingsSelectionListTile<SlideshowInterval>( SettingsSelectionListTile<SlideshowInterval>(
values: SlideshowInterval.values, values: SlideshowInterval.values,
getName: (context, v) => v.getName(context), getName: (context, v) => v.getName(context),
selector: (context, s) => s.screenSaverInterval, selector: (context, s) => s.screenSaverInterval,
onSelection: (v) => settings.screenSaverInterval = v, onSelection: (v) => settings.screenSaverInterval = v,
tileTitle: context.l10n.settingsSlideshowIntervalTile, tileTitle: l10n.settingsSlideshowIntervalTile,
dialogTitle: context.l10n.settingsSlideshowIntervalTitle, dialogTitle: l10n.settingsSlideshowIntervalTitle,
), ),
SettingsSelectionListTile<SlideshowVideoPlayback>( SettingsSelectionListTile<SlideshowVideoPlayback>(
values: SlideshowVideoPlayback.values, values: SlideshowVideoPlayback.values,
getName: (context, v) => v.getName(context), getName: (context, v) => v.getName(context),
selector: (context, s) => s.screenSaverVideoPlayback, selector: (context, s) => s.screenSaverVideoPlayback,
onSelection: (v) => settings.screenSaverVideoPlayback = v, onSelection: (v) => settings.screenSaverVideoPlayback = v,
tileTitle: context.l10n.settingsSlideshowVideoPlaybackTile, tileTitle: l10n.settingsSlideshowVideoPlaybackTile,
dialogTitle: context.l10n.settingsSlideshowVideoPlaybackTitle, 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( final originalCollection = CollectionLens(
source: source, source: source,
// TODO TLAD [screensaver] custom filters filters: settings.screenSaverCollectionFilters,
); );
var entries = originalCollection.sortedEntries; var entries = originalCollection.sortedEntries;
if (settings.screenSaverVideoPlayback == SlideshowVideoPlayback.skip) { if (settings.screenSaverVideoPlayback == SlideshowVideoPlayback.skip) {

View file

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

View file

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