Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2022-07-24 23:08:06 +02:00
commit 968f6da395
238 changed files with 4807 additions and 1683 deletions

View file

@ -17,8 +17,8 @@ jobs:
# Available versions may lag behind https://github.com/flutter/flutter.git # Available versions may lag behind https://github.com/flutter/flutter.git
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
flutter-version: '3.0.2' flutter-version: '3.3.0-0.0.pre'
channel: 'stable' channel: 'beta'
- name: Clone the repository. - name: Clone the repository.
uses: actions/checkout@v2 uses: actions/checkout@v2

View file

@ -19,8 +19,8 @@ jobs:
# Available versions may lag behind https://github.com/flutter/flutter.git # Available versions may lag behind https://github.com/flutter/flutter.git
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
flutter-version: '3.0.2' flutter-version: '3.3.0-0.0.pre'
channel: 'stable' channel: 'beta'
# Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1): # Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1):
# https://issuetracker.google.com/issues/144111441 # https://issuetracker.google.com/issues/144111441
@ -56,15 +56,15 @@ jobs:
rm release.keystore.asc rm release.keystore.asc
mkdir outputs mkdir outputs
(cd scripts/; ./apply_flavor_play.sh) (cd scripts/; ./apply_flavor_play.sh)
flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.0.2.sksl.json flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.3.0-0.0.pre.sksl.json
cp build/app/outputs/bundle/playRelease/*.aab outputs cp build/app/outputs/bundle/playRelease/*.aab outputs
flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.0.2.sksl.json flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.3.0-0.0.pre.sksl.json
cp build/app/outputs/apk/play/release/*.apk outputs cp build/app/outputs/apk/play/release/*.apk outputs
(cd scripts/; ./apply_flavor_huawei.sh) (cd scripts/; ./apply_flavor_huawei.sh)
flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_3.0.2.sksl.json flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_3.3.0-0.0.pre.sksl.json
cp build/app/outputs/apk/huawei/release/*.apk outputs cp build/app/outputs/apk/huawei/release/*.apk outputs
(cd scripts/; ./apply_flavor_izzy.sh) (cd scripts/; ./apply_flavor_izzy.sh)
flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_3.0.2.sksl.json flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_3.3.0-0.0.pre.sksl.json
cp build/app/outputs/apk/izzy/release/*.apk outputs cp build/app/outputs/apk/izzy/release/*.apk outputs
rm $AVES_STORE_FILE rm $AVES_STORE_FILE
env: env:

View file

@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
## <a id="v1.6.10"></a>[v1.6.10] - 2022-07-24
### Added
- Search: `on this day` and month filters in date filter section
- Stats: histogram and contextual date filters
- Screen saver
- Widget: photo frame
### Changed
- viewer: black background when overlay is disabled with light theme
- filter chip long press menu shows full label
- upgraded Flutter to beta v3.3.0-0.0.pre
### Fixed
- analysis service stuck when storage has ambiguous directories
## <a id="v1.6.9"></a>[v1.6.9] - 2022-06-18 ## <a id="v1.6.9"></a>[v1.6.9] - 2022-06-18
### Added ### Added

View file

@ -37,7 +37,7 @@ It scans your media collection to identify **motion photos**, **panoramas** (aka
**Navigation and search** is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc. **Navigation and search** is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc.
Aves integrates with Android (from **API 19 to 32**, i.e. from KitKat to Android 12L) with features such as **app shortcuts** and **global search** handling. It also works as a **media viewer and picker**. Aves integrates with Android (from **API 19 to 33**, i.e. from KitKat to Android 13) with features such as **widgets**, **app shortcuts**, **screen saver** and **global search** handling. It also works as a **media viewer and picker**.
## Screenshots ## Screenshots
@ -101,7 +101,7 @@ Some users have expressed the wish to financially support the project. Thanks!
[<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/paypal-badge-cropped.png" [<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/paypal-badge-cropped.png"
alt='Donate with PayPal' alt='Donate with PayPal'
height="40">](https://paypal.me/ThibaultDeckers) height="40">](https://paypal.me/ThibaultDeckersFr)
[<img src="https://liberapay.com/assets/widgets/donate.svg" [<img src="https://liberapay.com/assets/widgets/donate.svg"
alt='Donate using Liberapay' alt='Donate using Liberapay'
height="40">](https://liberapay.com/deckerst/donate) height="40">](https://liberapay.com/deckerst/donate)

View file

@ -5,18 +5,25 @@
"DE":"connect-dre.dbankcloud.cn", "DE":"connect-dre.dbankcloud.cn",
"DE_back":"connect-dre.hispace.hicloud.com", "DE_back":"connect-dre.hispace.hicloud.com",
"RU":"connect-drru.hispace.dbankcloud.ru", "RU":"connect-drru.hispace.dbankcloud.ru",
"RU_back":"connect-drru.hispace.dbankcloud.ru", "RU_back":"connect-drru.hispace.dbankcloud.cn",
"SG":"connect-dra.dbankcloud.cn", "SG":"connect-dra.dbankcloud.cn",
"SG_back":"connect-dra.hispace.hicloud.com" "SG_back":"connect-dra.hispace.hicloud.com"
}, },
"websocketgw_all":{
"CN":"connect-ws-drcn.hispace.dbankcloud.cn",
"CN_back":"connect-ws-drcn.hispace.dbankcloud.com",
"DE":"connect-ws-dre.hispace.dbankcloud.cn",
"DE_back":"connect-ws-dre.hispace.dbankcloud.com",
"RU":"connect-ws-drru.hispace.dbankcloud.ru",
"RU_back":"connect-ws-drru.hispace.dbankcloud.cn",
"SG":"connect-ws-dra.hispace.dbankcloud.cn",
"SG_back":"connect-ws-dra.hispace.dbankcloud.com"
},
"client":{ "client":{
"cp_id":"2640082000020010713", "cp_id":"2640082000020010713",
"product_id":"99536292102197525", "product_id":"99536292102197525",
"client_id":"874325707927340288",
"client_secret":"DCAFAE5C0440ABDBD6DDB2B6EBD7D9B0870C10FCA64759CCD63020D168803AB5",
"project_id":"99536292102197525", "project_id":"99536292102197525",
"app_id":"106014023", "app_id":"106014023",
"api_key":"DAEDAEzScQA5ri36P2NEiVPSFrOJeYZ0DbEJZMGJrBadW+QudBr5BGHD3vO0tsL1VeBy0RPZefPic3hAWUijcBxCv0zRv0iBjQEptQ==",
"package_name":"deckers.thibault.aves" "package_name":"deckers.thibault.aves"
}, },
"oauth_client":{ "oauth_client":{
@ -30,17 +37,17 @@
"configuration_version":"3.0", "configuration_version":"3.0",
"appInfos":[ "appInfos":[
{ {
"package_name":"deckers.thibault.aves.profile", "package_name":"deckers.thibault.aves",
"client":{ "client":{
"app_id":"106031461" "app_id":"106014023"
}, },
"app_info":{ "app_info":{
"package_name":"deckers.thibault.aves.profile", "package_name":"deckers.thibault.aves",
"app_id":"106031461" "app_id":"106014023"
}, },
"oauth_client":{ "oauth_client":{
"client_type":1, "client_type":1,
"client_id":"106031461" "client_id":"106014023"
} }
}, },
{ {
@ -58,17 +65,17 @@
} }
}, },
{ {
"package_name":"deckers.thibault.aves", "package_name":"deckers.thibault.aves.profile",
"client":{ "client":{
"app_id":"106014023" "app_id":"106031461"
}, },
"app_info":{ "app_info":{
"package_name":"deckers.thibault.aves", "package_name":"deckers.thibault.aves.profile",
"app_id":"106014023" "app_id":"106031461"
}, },
"oauth_client":{ "oauth_client":{
"client_type":1, "client_type":1,
"client_id":"106014023" "client_id":"106031461"
} }
} }
] ]

View file

@ -38,6 +38,7 @@ if (keystorePropertiesFile.exists()) {
keystoreProperties['keyAlias'] = System.getenv('AVES_KEY_ALIAS') keystoreProperties['keyAlias'] = System.getenv('AVES_KEY_ALIAS')
keystoreProperties['keyPassword'] = System.getenv('AVES_KEY_PASSWORD') keystoreProperties['keyPassword'] = System.getenv('AVES_KEY_PASSWORD')
keystoreProperties['googleApiKey'] = System.getenv('AVES_GOOGLE_API_KEY') keystoreProperties['googleApiKey'] = System.getenv('AVES_GOOGLE_API_KEY')
keystoreProperties['huaweiApiKey'] = System.getenv('AVES_HUAWEI_API_KEY')
} }
android { android {
@ -47,7 +48,6 @@ android {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
} }
defaultConfig { defaultConfig {
applicationId appId applicationId appId
// minSdkVersion constraints: // minSdkVersion constraints:
@ -60,7 +60,8 @@ android {
targetSdkVersion 33 targetSdkVersion 33
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey']] manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey'],
huaweiApiKey: keystoreProperties['huaweiApiKey']]
multiDexEnabled true multiDexEnabled true
resValue 'string', 'search_provider', "${appId}.search_provider" resValue 'string', 'search_provider', "${appId}.search_provider"
} }
@ -140,7 +141,6 @@ android {
lint { lint {
disable 'InvalidPackage' disable 'InvalidPackage'
} }
namespace 'deckers.thibault.aves'
} }
flutter { flutter {
@ -169,7 +169,7 @@ dependencies {
// huawei flavor only // huawei flavor only
huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.5.2.300' huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.5.2.300'
kapt 'androidx.annotation:annotation:1.3.0' kapt 'androidx.annotation:annotation:1.4.0'
kapt 'com.github.bumptech.glide:compiler:4.13.0' kapt 'com.github.bumptech.glide:compiler:4.13.0'
compileOnly rootProject.findProject(':streams_channel') compileOnly rootProject.findProject(':streams_channel')

View file

@ -0,0 +1,2 @@
<dream xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="deckers.thibault.aves.debug/deckers.thibault.aves.ScreenSaverSettingsActivity" />

View file

@ -1,5 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Android Studio Chipmunk (2021.2.1) recommends:
- removing "package" from AndroidManifest.xml
- adding it as "namespace" in app/build.gradle
This change eventually prevents building the app with Flutter v3.0.2.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="deckers.thibault.aves"
android:installLocation="auto"> android:installLocation="auto">
<!-- <!--
@ -134,6 +143,11 @@
android:resource="@xml/searchable" /> android:resource="@xml/searchable" />
</activity> </activity>
<activity
android:name=".ScreenSaverSettingsActivity"
android:exported="true"
android:theme="@style/NormalTheme" />
<activity <activity
android:name=".WallpaperActivity" android:name=".WallpaperActivity"
android:exported="true" android:exported="true"
@ -153,11 +167,49 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".HomeWidgetSettingsActivity"
android:exported="false"
android:theme="@style/NormalTheme">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<receiver
android:name=".HomeWidgetProvider"
android:exported="false"
android:label="@string/app_widget_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/app_widget_info" />
</receiver>
<service <service
android:name=".AnalysisService" android:name=".AnalysisService"
android:description="@string/analysis_service_description" android:description="@string/analysis_service_description"
android:exported="false" /> android:exported="false" />
<service
android:name=".ScreenSaverService"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:permission="android.permission.BIND_DREAM_SERVICE">
<intent-filter>
<action android:name="android.service.dreams.DreamService" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.service.dream"
android:resource="@xml/screen_saver" />
</service>
<!-- file provider to share files having a file:// URI --> <!-- file provider to share files having a file:// URI -->
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
@ -178,6 +230,9 @@
<meta-data <meta-data
android:name="com.google.android.geo.API_KEY" android:name="com.google.android.geo.API_KEY"
android:value="${googleApiKey}" /> android:value="${googleApiKey}" />
<meta-data
android:name="deckers.thibault.aves.huawei.API_KEY"
android:value="${huaweiApiKey}" />
<meta-data <meta-data
android:name="firebase_crashlytics_collection_enabled" android:name="firebase_crashlytics_collection_enabled"
android:value="false" /> android:value="false" />

View file

@ -11,10 +11,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import app.loup.streams_channel.StreamsChannel import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.MainActivity.Companion.OPEN_FROM_ANALYSIS_SERVICE import deckers.thibault.aves.MainActivity.Companion.OPEN_FROM_ANALYSIS_SERVICE
import deckers.thibault.aves.channel.calls.DeviceHandler import deckers.thibault.aves.channel.calls.*
import deckers.thibault.aves.channel.calls.GeocodingHandler
import deckers.thibault.aves.channel.calls.MediaStoreHandler
import deckers.thibault.aves.channel.calls.MetadataFetchHandler
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
import deckers.thibault.aves.utils.FlutterUtils import deckers.thibault.aves.utils.FlutterUtils
@ -25,7 +22,7 @@ import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
class AnalysisService : MethodChannel.MethodCallHandler, Service() { class AnalysisService : MethodChannel.MethodCallHandler, Service() {
private var backgroundFlutterEngine: FlutterEngine? = null private var flutterEngine: FlutterEngine? = null
private var backgroundChannel: MethodChannel? = null private var backgroundChannel: MethodChannel? = null
private var serviceLooper: Looper? = null private var serviceLooper: Looper? = null
private var serviceHandler: ServiceHandler? = null private var serviceHandler: ServiceHandler? = null
@ -37,18 +34,27 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
runBlocking { runBlocking {
FlutterUtils.initFlutterEngine(context, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) { FlutterUtils.initFlutterEngine(context, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) {
backgroundFlutterEngine = it flutterEngine = it
} }
} }
val messenger = backgroundFlutterEngine!!.dartExecutor.binaryMessenger val messenger = flutterEngine!!.dartExecutor
// channels for analysis // channels for analysis
// dart -> platform -> dart
// - need Context
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this)) MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this)) MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
// result streaming: dart -> platform ->->-> dart
// - need Context
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) } StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) } StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) }
// channel for service management // channel for service management
backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply { backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply {
setMethodCallHandler(context) setMethodCallHandler(context)
@ -67,7 +73,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
override fun onBind(intent: Intent) = analysisServiceBinder override fun onBind(intent: Intent) = analysisServiceBinder
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val channel = NotificationChannelCompat.Builder(CHANNEL_ANALYSIS, NotificationManagerCompat.IMPORTANCE_LOW) val channel = NotificationChannelCompat.Builder(CHANNEL_ANALYSIS, NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getText(R.string.analysis_channel_name)) .setName(getText(R.string.analysis_channel_name))
.setShowBadge(false) .setShowBadge(false)
@ -76,7 +82,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
startForeground(NOTIFICATION_ID, buildNotification()) startForeground(NOTIFICATION_ID, buildNotification())
val msgData = Bundle() val msgData = Bundle()
intent.extras?.let { intent?.extras?.let {
msgData.putAll(it) msgData.putAll(it)
} }
serviceHandler?.obtainMessage()?.let { msg -> serviceHandler?.obtainMessage()?.let { msg ->

View file

@ -0,0 +1,61 @@
package deckers.thibault.aves
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.Bundle
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class HomeWidgetSettingsActivity : MainActivity() {
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// cancel if user does not complete widget setup
setResult(RESULT_CANCELED)
intent.extras?.let {
appWidgetId = it.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
intentDataMap = extractIntentData(intent)
}
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish()
return
}
val messenger = flutterEngine!!.dartExecutor
MethodChannel(messenger, CHANNEL).setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
when (call.method) {
"configure" -> {
result.success(null)
saveWidget()
}
else -> result.notImplemented()
}
}
}
private fun saveWidget() {
val appWidgetManager = AppWidgetManager.getInstance(context)
val widgetInfo = appWidgetManager.getAppWidgetOptions(appWidgetId)
HomeWidgetProvider().onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, widgetInfo)
val intent = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
setResult(RESULT_OK, intent)
finish()
}
override fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_WIDGET_SETTINGS,
INTENT_DATA_KEY_WIDGET_ID to appWidgetId,
)
}
companion object {
private const val CHANNEL = "deckers.thibault/aves/widget_configure"
}
}

View file

@ -0,0 +1,202 @@
package deckers.thibault.aves
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.RemoteViews
import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.calls.DeviceHandler
import deckers.thibault.aves.channel.calls.MediaFetchHandler
import deckers.thibault.aves.channel.calls.MediaStoreHandler
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
import deckers.thibault.aves.utils.FlutterUtils
import deckers.thibault.aves.utils.LogUtils
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.*
import java.nio.ByteBuffer
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.math.roundToInt
class HomeWidgetProvider : AppWidgetProvider() {
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
Log.d(LOG_TAG, "Widget onUpdate widgetIds=${appWidgetIds.contentToString()}")
for (widgetId in appWidgetIds) {
val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId)
defaultScope.launch {
val backgroundBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = false)
updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, backgroundBytes)
val imageBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false)
updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageBytes)
}
}
}
override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager?, widgetId: Int, widgetInfo: Bundle?) {
Log.d(LOG_TAG, "Widget onAppWidgetOptionsChanged widgetId=$widgetId")
appWidgetManager ?: return
widgetInfo ?: return
if (imageByteFetchJob != null) {
imageByteFetchJob?.cancel()
}
imageByteFetchJob = defaultScope.launch {
delay(500)
val imageBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = true)
updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageBytes)
}
}
private suspend fun getBytes(
context: Context,
widgetId: Int,
widgetInfo: Bundle,
drawEntryImage: Boolean,
reuseEntry: Boolean = false,
): ByteArray? {
val devicePixelRatio = context.resources.displayMetrics.density
val widthPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) * devicePixelRatio).roundToInt()
val heightPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) * devicePixelRatio).roundToInt()
if (widthPx == 0 || heightPx == 0) return null
initFlutterEngine(context)
val messenger = flutterEngine!!.dartExecutor
val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL)
try {
val bytes = suspendCoroutine { cont ->
defaultScope.launch {
FlutterUtils.runOnUiThread {
channel.invokeMethod("drawWidget", hashMapOf(
"widgetId" to widgetId,
"widthPx" to widthPx,
"heightPx" to heightPx,
"devicePixelRatio" to devicePixelRatio,
"drawEntryImage" to drawEntryImage,
"reuseEntry" to reuseEntry,
), object : MethodChannel.Result {
override fun success(result: Any?) {
cont.resume(result)
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
cont.resumeWithException(Exception("$errorCode: $errorMessage\n$errorDetails"))
}
override fun notImplemented() {
cont.resumeWithException(Exception("not implemented"))
}
})
}
}
}
if (bytes is ByteArray) return bytes
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to draw widget for widgetId=$widgetId widthPx=$widthPx heightPx=$heightPx", e)
}
return null
}
private fun updateWidgetImage(
context: Context,
appWidgetManager: AppWidgetManager,
widgetId: Int,
widgetInfo: Bundle,
bytes: ByteArray?,
) {
bytes ?: return
val devicePixelRatio = context.resources.displayMetrics.density
val widthPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) * devicePixelRatio).roundToInt()
val heightPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) * devicePixelRatio).roundToInt()
try {
val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(bytes))
// set a unique URI to prevent the intent (and its extras) from being shared by different widgets
val intent = Intent(MainActivity.INTENT_ACTION_WIDGET_OPEN, Uri.parse("widget://$widgetId"), context, MainActivity::class.java)
.putExtra(MainActivity.EXTRA_KEY_WIDGET_ID, widgetId)
val activity = PendingIntent.getActivity(
context,
0,
intent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
val views = RemoteViews(context.packageName, R.layout.app_widget).apply {
setImageViewBitmap(R.id.widget_img, bitmap)
setOnClickPendingIntent(R.id.widget_img, activity)
}
appWidgetManager.updateAppWidget(widgetId, views)
bitmap.recycle()
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to draw widget", e)
}
}
companion object {
private val LOG_TAG = LogUtils.createTag<HomeWidgetProvider>()
private const val WIDGET_DART_ENTRYPOINT = "widgetMain"
private const val WIDGET_DRAW_CHANNEL = "deckers.thibault/aves/widget_draw"
private var flutterEngine: FlutterEngine? = null
private var imageByteFetchJob: Job? = null
private suspend fun initFlutterEngine(context: Context) {
if (flutterEngine != null) return
FlutterUtils.runOnUiThread {
flutterEngine = FlutterEngine(context.applicationContext)
}
initChannels(context)
flutterEngine!!.apply {
if (!dartExecutor.isExecutingDart) {
val appBundlePathOverride = FlutterInjector.instance().flutterLoader().findAppBundlePath()
val entrypoint = DartExecutor.DartEntrypoint(appBundlePathOverride, WIDGET_DART_ENTRYPOINT)
FlutterUtils.runOnUiThread {
dartExecutor.executeDartEntrypoint(entrypoint)
}
}
}
}
private fun initChannels(context: Context) {
val messenger = flutterEngine!!.dartExecutor
// dart -> platform -> dart
// - need Context
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context))
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context))
MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(context))
// result streaming: dart -> platform ->->-> dart
// - need Context
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(context, args) }
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(context, args) }
}
}
}

View file

@ -2,10 +2,14 @@ package deckers.thibault.aves
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.SearchManager import android.app.SearchManager
import android.appwidget.AppWidgetManager
import android.content.ClipData import android.content.ClipData
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.* import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
@ -13,6 +17,8 @@ import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import app.loup.streams_channel.StreamsChannel import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.calls.* import deckers.thibault.aves.channel.calls.*
import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
import deckers.thibault.aves.channel.calls.window.WindowHandler
import deckers.thibault.aves.channel.streams.* import deckers.thibault.aves.channel.streams.*
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat import deckers.thibault.aves.utils.getParcelableExtraCompat
@ -23,12 +29,12 @@ import io.flutter.plugin.common.MethodChannel
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
class MainActivity : FlutterActivity() { open class MainActivity : FlutterActivity() {
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler
private lateinit var intentStreamHandler: IntentStreamHandler private lateinit var intentStreamHandler: IntentStreamHandler
private lateinit var analysisStreamHandler: AnalysisStreamHandler private lateinit var analysisStreamHandler: AnalysisStreamHandler
private lateinit var intentDataMap: MutableMap<String, Any?> internal lateinit var intentDataMap: MutableMap<String, Any?>
private lateinit var analysisHandler: AnalysisHandler private lateinit var analysisHandler: AnalysisHandler
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -51,7 +57,7 @@ class MainActivity : FlutterActivity() {
// ) // )
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val messenger = flutterEngine!!.dartExecutor.binaryMessenger val messenger = flutterEngine!!.dartExecutor
// dart -> platform -> dart // dart -> platform -> dart
// - need Context // - need Context
@ -63,14 +69,17 @@ class MainActivity : FlutterActivity() {
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this)) MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this)) MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
MethodChannel(messenger, GlobalSearchHandler.CHANNEL).setMethodCallHandler(GlobalSearchHandler(this)) MethodChannel(messenger, GlobalSearchHandler.CHANNEL).setMethodCallHandler(GlobalSearchHandler(this))
MethodChannel(messenger, HomeWidgetHandler.CHANNEL).setMethodCallHandler(HomeWidgetHandler(this))
MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(this))
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this)) MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
// - need Activity // - need ContextWrapper
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this)) MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this)) MethodChannel(messenger, MediaEditHandler.CHANNEL).setMethodCallHandler(MediaEditHandler(this))
MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this)) MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this))
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(WindowHandler(this)) // - need Activity
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
// result streaming: dart -> platform ->->-> dart // result streaming: dart -> platform ->->-> dart
// - need Context // - need Context
@ -78,7 +87,7 @@ 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 {
@ -93,15 +102,16 @@ 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)
} }
} }
@ -156,27 +166,33 @@ 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 {
@ -197,18 +213,11 @@ class MainActivity : FlutterActivity() {
} }
} }
private fun extractIntentData(intent: Intent?): MutableMap<String, Any?> { open fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
when (intent?.action) { when (val action = 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,
@ -228,7 +237,7 @@ 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),
) )
@ -244,6 +253,22 @@ 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 action,
INTENT_DATA_KEY_FILTERS to initialFilters,
)
}
INTENT_ACTION_WIDGET_OPEN -> {
val widgetId = intent.getIntExtra(EXTRA_KEY_WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
if (widgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
return hashMapOf(
INTENT_DATA_KEY_ACTION to action,
INTENT_DATA_KEY_WIDGET_ID to widgetId,
)
}
}
Intent.ACTION_RUN -> { Intent.ACTION_RUN -> {
// flutter run // flutter run
} }
@ -254,7 +279,22 @@ 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)) }
@ -278,6 +318,19 @@ 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
@ -291,7 +344,7 @@ 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()
@ -300,7 +353,7 @@ 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()
@ -314,7 +367,7 @@ 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
@ -322,29 +375,39 @@ 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_ACTION_PICK_ITEMS = "pick_items"
const val INTENT_DATA_KEY_FILTERS = "filters" const val INTENT_ACTION_PICK_COLLECTION_FILTERS = "pick_collection_filters"
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType" const val INTENT_ACTION_SCREEN_SAVER = "screen_saver"
const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple" const val INTENT_ACTION_SCREEN_SAVER_SETTINGS = "screen_saver_settings"
const val INTENT_DATA_KEY_PAGE = "page"
const val INTENT_DATA_KEY_URI = "uri"
const val INTENT_DATA_KEY_QUERY = "query"
const val INTENT_ACTION_PICK = "pick"
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 INTENT_ACTION_WIDGET_OPEN = "widget_open"
const val INTENT_ACTION_WIDGET_SETTINGS = "widget_settings"
const val SHORTCUT_KEY_PAGE = "page" const val INTENT_DATA_KEY_ACTION = "action"
const val SHORTCUT_KEY_FILTERS_ARRAY = "filters" const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
const val SHORTCUT_KEY_FILTERS_STRING = "filtersString" const val INTENT_DATA_KEY_FILTERS = "filters"
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
const val INTENT_DATA_KEY_PAGE = "page"
const val INTENT_DATA_KEY_QUERY = "query"
const val INTENT_DATA_KEY_URI = "uri"
const val INTENT_DATA_KEY_WIDGET_ID = "widgetId"
const val EXTRA_KEY_PAGE = "page"
const val EXTRA_KEY_FILTERS_ARRAY = "filters"
const val EXTRA_KEY_FILTERS_STRING = "filtersString"
const val EXTRA_KEY_WIDGET_ID = "widgetId"
// 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

@ -0,0 +1,136 @@
package deckers.thibault.aves
import android.service.dreams.DreamService
import android.util.Log
import android.view.View
import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.calls.*
import deckers.thibault.aves.channel.calls.window.ServiceWindowHandler
import deckers.thibault.aves.channel.calls.window.WindowHandler
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
import deckers.thibault.aves.utils.LogUtils
import io.flutter.FlutterInjector
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.FlutterSurfaceView
import io.flutter.embedding.android.FlutterView
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint
import io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister
import io.flutter.plugin.common.MethodChannel
// for FlutterView-level integration, cf https://docs.flutter.dev/development/add-to-app/android/add-flutter-view
class ScreenSaverService : DreamService() {
private var flutterEngine: FlutterEngine? = null
private var flutterView: FlutterView? = null
override fun onAttachedToWindow() {
Log.i(LOG_TAG, "onAttachedToWindow")
super.onAttachedToWindow()
initDream()
createEngine()
setContentView(createView())
}
override fun onDreamingStarted() {
Log.i(LOG_TAG, "onDreamingStarted")
super.onDreamingStarted()
onStart()
}
override fun onDreamingStopped() {
Log.i(LOG_TAG, "onDreamingStopped")
release()
super.onDreamingStopped()
}
override fun onDetachedFromWindow() {
Log.i(LOG_TAG, "onDetachedFromWindow")
destroyView()
super.onDetachedFromWindow()
}
private fun initDream() {
isInteractive = false
isFullscreen = true
}
private fun createEngine() {
flutterEngine = flutterEngine ?: FlutterEngine(this, null, false)
GeneratedPluginRegister.registerGeneratedPlugins(flutterEngine!!)
initChannels()
}
private fun createView(): View {
flutterView = FlutterView(this, FlutterSurfaceView(this)).apply {
id = FlutterActivity.FLUTTER_VIEW_ID
attachToFlutterEngine(flutterEngine!!)
}
return flutterView!!
}
private fun destroyView() {
flutterEngine?.lifecycleChannel?.appIsDetached()
flutterView?.detachFromFlutterEngine()
}
private fun release() {
destroyView()
flutterEngine = null
flutterView = null
}
private fun onStart() {
flutterEngine!!.apply {
if (!dartExecutor.isExecutingDart) {
navigationChannel.setInitialRoute(DEFAULT_INITIAL_ROUTE)
val appBundlePathOverride = FlutterInjector.instance().flutterLoader().findAppBundlePath()
val entrypoint = DartEntrypoint(appBundlePathOverride, DEFAULT_DART_ENTRYPOINT)
dartExecutor.executeDartEntrypoint(entrypoint)
}
lifecycleChannel.appIsResumed()
}
}
private fun initChannels() {
val messenger = flutterEngine!!.dartExecutor
// dart -> platform -> dart
// - need Context
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(this))
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
// - need ContextWrapper
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
// - need Service
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ServiceWindowHandler(this))
// result streaming: dart -> platform ->->-> dart
// - need Context
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) }
// intent handling
// detail fetch: dart -> platform
MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"getIntentData" -> {
result.success(intentDataMap)
}
}
}
}
companion object {
private val LOG_TAG = LogUtils.createTag<ScreenSaverService>()
private val intentDataMap: Map<String, Any?> = hashMapOf(
MainActivity.INTENT_DATA_KEY_ACTION to MainActivity.INTENT_ACTION_SCREEN_SAVER,
)
// from `FlutterActivityLaunchConfigs`
const val DEFAULT_DART_ENTRYPOINT = "main"
const val DEFAULT_INITIAL_ROUTE = "/"
}
}

View file

@ -0,0 +1,11 @@
package deckers.thibault.aves
import android.content.Intent
class ScreenSaverSettingsActivity : MainActivity() {
override fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_SCREEN_SAVER_SETTINGS,
)
}
}

View file

@ -23,7 +23,7 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvider() { class SearchSuggestionsProvider : ContentProvider() {
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? { override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
@ -67,15 +67,23 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid
} }
private suspend fun getSuggestions(context: Context, query: String): List<FieldMap> { private suspend fun getSuggestions(context: Context, query: String): List<FieldMap> {
if (backgroundFlutterEngine == null) { if (flutterEngine == null) {
FlutterUtils.initFlutterEngine(context, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) { FlutterUtils.initFlutterEngine(context, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) {
backgroundFlutterEngine = it flutterEngine = it
} }
} }
val messenger = backgroundFlutterEngine!!.dartExecutor.binaryMessenger val messenger = flutterEngine!!.dartExecutor
val backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL) val backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL)
backgroundChannel.setMethodCallHandler(this) backgroundChannel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
when (call.method) {
"initialized" -> {
Log.d(LOG_TAG, "background channel is ready")
result.success(null)
}
else -> result.notImplemented()
}
}
try { try {
return suspendCoroutine { cont -> return suspendCoroutine { cont ->
@ -96,7 +104,7 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid
} }
override fun notImplemented() { override fun notImplemented() {
cont.resumeWithException(NotImplementedError("getSuggestions")) cont.resumeWithException(Exception("not implemented"))
} }
}) })
} }
@ -108,16 +116,6 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid
} }
} }
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"initialized" -> {
Log.d(LOG_TAG, "background channel is ready")
result.success(null)
}
else -> result.notImplemented()
}
}
override fun onCreate(): Boolean = true override fun onCreate(): Boolean = true
override fun getType(uri: Uri): String? = null override fun getType(uri: Uri): String? = null
@ -137,6 +135,6 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid
const val SHARED_PREFERENCES_KEY = "platform_search" const val SHARED_PREFERENCES_KEY = "platform_search"
const val CALLBACK_HANDLE_KEY = "callback_handle" const val CALLBACK_HANDLE_KEY = "callback_handle"
private var backgroundFlutterEngine: FlutterEngine? = null private var flutterEngine: FlutterEngine? = null
} }
} }

View file

@ -2,10 +2,15 @@ package deckers.thibault.aves
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.* import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import app.loup.streams_channel.StreamsChannel import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.calls.* import deckers.thibault.aves.channel.calls.*
import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
import deckers.thibault.aves.channel.calls.window.WindowHandler
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat import deckers.thibault.aves.utils.getParcelableExtraCompat
@ -23,18 +28,19 @@ class WallpaperActivity : FlutterActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val messenger = flutterEngine!!.dartExecutor.binaryMessenger val messenger = flutterEngine!!.dartExecutor
// dart -> platform -> dart // dart -> platform -> dart
// - need Context // - need Context
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this)) MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this)) MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, MediaFetchHandler.CHANNEL).setMethodCallHandler(MediaFetchHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
// - need Activity // - need ContextWrapper
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this)) MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this))
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this)) MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this))
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(WindowHandler(this)) // - need Activity
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
// result streaming: dart -> platform ->->-> dart // result streaming: dart -> platform ->->-> dart
// - need Context // - need Context
@ -43,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)
@ -67,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 -> {
@ -102,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

@ -1,8 +1,8 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
@ -13,7 +13,7 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
class AccessibilityHandler(private val activity: Activity) : MethodCallHandler { class AccessibilityHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"areAnimationsRemoved" -> safe(call, result, ::areAnimationsRemoved) "areAnimationsRemoved" -> safe(call, result, ::areAnimationsRemoved)
@ -28,7 +28,7 @@ class AccessibilityHandler(private val activity: Activity) : MethodCallHandler {
@SuppressLint("ObsoleteSdkInt") @SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
try { try {
removed = Settings.Global.getFloat(activity.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f removed = Settings.Global.getFloat(contextWrapper.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f
} catch (e: Exception) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null) Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
} }
@ -49,7 +49,7 @@ class AccessibilityHandler(private val activity: Activity) : MethodCallHandler {
val originalTimeoutMillis = call.argument<Int>("originalTimeoutMillis") val originalTimeoutMillis = call.argument<Int>("originalTimeoutMillis")
val content = call.argument<List<String>>("content") val content = call.argument<List<String>>("content")
if (originalTimeoutMillis == null || content == null) { if (originalTimeoutMillis == null || content == null) {
result.error("getRecommendedTimeoutMillis-args", "failed because of missing arguments", null) result.error("getRecommendedTimeoutMillis-args", "missing arguments", null)
return return
} }
@ -66,7 +66,7 @@ class AccessibilityHandler(private val activity: Activity) : MethodCallHandler {
} }
} }
val am = activity.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager val am = contextWrapper.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
if (am == null) { if (am == null) {
result.error("getRecommendedTimeoutMillis-service", "failed to get accessibility manager", null) result.error("getRecommendedTimeoutMillis-service", "failed to get accessibility manager", null)
return return

View file

@ -16,7 +16,10 @@ import deckers.thibault.aves.utils.ContextUtils.isMyServiceRunning
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class AnalysisHandler(private val activity: Activity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler, AnalysisServiceListener { class AnalysisHandler(private val activity: Activity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler, AnalysisServiceListener {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@ -33,7 +36,7 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
private fun registerCallback(call: MethodCall, result: MethodChannel.Result) { private fun registerCallback(call: MethodCall, result: MethodChannel.Result) {
val callbackHandle = call.argument<Number>("callbackHandle")?.toLong() val callbackHandle = call.argument<Number>("callbackHandle")?.toLong()
if (callbackHandle == null) { if (callbackHandle == null) {
result.error("registerCallback-args", "failed because of missing arguments", null) result.error("registerCallback-args", "missing arguments", null)
return return
} }
@ -47,7 +50,7 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
private fun startAnalysis(call: MethodCall, result: MethodChannel.Result) { private fun startAnalysis(call: MethodCall, result: MethodChannel.Result) {
val force = call.argument<Boolean>("force") val force = call.argument<Boolean>("force")
if (force == null) { if (force == null) {
result.error("startAnalysis-args", "failed because of missing arguments", null) result.error("startAnalysis-args", "missing arguments", null)
return return
} }
@ -56,9 +59,9 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
if (!activity.isMyServiceRunning(AnalysisService::class.java)) { if (!activity.isMyServiceRunning(AnalysisService::class.java)) {
val intent = Intent(activity, AnalysisService::class.java) val intent = Intent(activity, AnalysisService::class.java)
intent.putExtra(AnalysisService.KEY_COMMAND, AnalysisService.COMMAND_START) .putExtra(AnalysisService.KEY_COMMAND, AnalysisService.COMMAND_START)
intent.putExtra(AnalysisService.KEY_ENTRY_IDS, entryIds?.toIntArray()) .putExtra(AnalysisService.KEY_ENTRY_IDS, entryIds?.toIntArray())
intent.putExtra(AnalysisService.KEY_FORCE, force) .putExtra(AnalysisService.KEY_FORCE, force)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.startForegroundService(intent) activity.startForegroundService(intent)
} else { } else {

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
@ -142,7 +142,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val packageName = call.argument<String>("packageName") val packageName = call.argument<String>("packageName")
val sizeDip = call.argument<Number>("sizeDip")?.toDouble() val sizeDip = call.argument<Number>("sizeDip")?.toDouble()
if (packageName == null || sizeDip == null) { if (packageName == null || sizeDip == null) {
result.error("getAppIcon-args", "failed because of missing arguments", null) result.error("getAppIcon-args", "missing arguments", null)
return return
} }
@ -208,7 +208,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val label = call.argument<String>("label") val label = call.argument<String>("label")
if (uri == null) { if (uri == null) {
result.error("copyToClipboard-args", "failed because of missing arguments", null) result.error("copyToClipboard-args", "missing arguments", null)
return return
} }
@ -235,7 +235,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
if (uri == null) { if (uri == null) {
result.error("edit-args", "failed because of missing arguments", null) result.error("edit-args", "missing arguments", null)
return return
} }
@ -252,7 +252,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
if (uri == null) { if (uri == null) {
result.error("open-args", "failed because of missing arguments", null) result.error("open-args", "missing arguments", null)
return return
} }
@ -267,7 +267,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
private fun openMap(call: MethodCall, result: MethodChannel.Result) { private fun openMap(call: MethodCall, result: MethodChannel.Result) {
val geoUri = call.argument<String>("geoUri")?.let { Uri.parse(it) } val geoUri = call.argument<String>("geoUri")?.let { Uri.parse(it) }
if (geoUri == null) { if (geoUri == null) {
result.error("openMap-args", "failed because of missing arguments", null) result.error("openMap-args", "missing arguments", null)
return return
} }
@ -282,7 +282,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
if (uri == null) { if (uri == null) {
result.error("setAs-args", "failed because of missing arguments", null) result.error("setAs-args", "missing arguments", null)
return return
} }
@ -298,7 +298,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val title = call.argument<String>("title") val title = call.argument<String>("title")
val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType") val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType")
if (urisByMimeType == null) { if (urisByMimeType == null) {
result.error("setAs-args", "failed because of missing arguments", null) result.error("setAs-args", "missing arguments", null)
return return
} }
@ -378,7 +378,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val filters = call.argument<List<String>>("filters") val filters = call.argument<List<String>>("filters")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (label == null || (filters == null && uri == null)) { if (label == null || (filters == null && uri == null)) {
result.error("pin-args", "failed because of missing arguments", null) result.error("pin-args", "missing arguments", null)
return return
} }
@ -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

@ -138,7 +138,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private fun getBitmapFactoryInfo(call: MethodCall, result: MethodChannel.Result) { private fun getBitmapFactoryInfo(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (uri == null) { if (uri == null) {
result.error("getBitmapDecoderInfo-args", "failed because of missing arguments", null) result.error("getBitmapDecoderInfo-args", "missing arguments", null)
return return
} }
@ -167,7 +167,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getContentResolverMetadata-args", "failed because of missing arguments", null) result.error("getContentResolverMetadata-args", "missing arguments", null)
return return
} }
@ -224,7 +224,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getExifInterfaceMetadata-args", "failed because of missing arguments", null) result.error("getExifInterfaceMetadata-args", "missing arguments", null)
return return
} }
@ -250,7 +250,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private fun getMediaMetadataRetrieverMetadata(call: MethodCall, result: MethodChannel.Result) { private fun getMediaMetadataRetrieverMetadata(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (uri == null) { if (uri == null) {
result.error("getMediaMetadataRetrieverMetadata-args", "failed because of missing arguments", null) result.error("getMediaMetadataRetrieverMetadata-args", "missing arguments", null)
return return
} }
@ -276,7 +276,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getMetadataExtractorSummary-args", "failed because of missing arguments", null) result.error("getMetadataExtractorSummary-args", "missing arguments", null)
return return
} }
@ -319,7 +319,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getPixyMetadata-args", "failed because of missing arguments", null) result.error("getPixyMetadata-args", "missing arguments", null)
return return
} }
@ -340,7 +340,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private fun getTiffStructure(call: MethodCall, result: MethodChannel.Result) { private fun getTiffStructure(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (uri == null) { if (uri == null) {
result.error("getTiffStructure-args", "failed because of missing arguments", null) result.error("getTiffStructure-args", "missing arguments", null)
return return
} }

View file

@ -57,7 +57,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getExifThumbnails-args", "failed because of missing arguments", null) result.error("getExifThumbnails-args", "missing arguments", null)
return return
} }
@ -88,7 +88,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
val displayName = call.argument<String>("displayName") val displayName = call.argument<String>("displayName")
if (mimeType == null || uri == null || sizeBytes == null) { if (mimeType == null || uri == null || sizeBytes == null) {
result.error("extractMotionPhotoVideo-args", "failed because of missing arguments", null) result.error("extractMotionPhotoVideo-args", "missing arguments", null)
return return
} }
@ -108,7 +108,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val displayName = call.argument<String>("displayName") val displayName = call.argument<String>("displayName")
if (uri == null) { if (uri == null) {
result.error("extractVideoEmbeddedPicture-args", "failed because of missing arguments", null) result.error("extractVideoEmbeddedPicture-args", "missing arguments", null)
return return
} }
@ -143,7 +143,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
val dataPropPath = call.argument<String>("propPath") val dataPropPath = call.argument<String>("propPath")
val embedMimeType = call.argument<String>("propMimeType") val embedMimeType = call.argument<String>("propMimeType")
if (mimeType == null || uri == null || dataPropPath == null || embedMimeType == null) { if (mimeType == null || uri == null || dataPropPath == null || embedMimeType == null) {
result.error("extractXmpDataProp-args", "failed because of missing arguments", null) result.error("extractXmpDataProp-args", "missing arguments", null)
return return
} }

View file

@ -35,7 +35,7 @@ class GeocodingHandler(private val context: Context) : MethodCallHandler {
val localeString = call.argument<String>("locale") val localeString = call.argument<String>("locale")
val maxResults = call.argument<Int>("maxResults") ?: 1 val maxResults = call.argument<Int>("maxResults") ?: 1
if (latitude == null || longitude == null) { if (latitude == null || longitude == null) {
result.error("getAddress-args", "failed because of missing arguments", null) result.error("getAddress-args", "missing arguments", null)
return return
} }

View file

@ -23,7 +23,7 @@ class GlobalSearchHandler(private val context: Context) : MethodCallHandler {
private fun registerCallback(call: MethodCall, result: MethodChannel.Result) { private fun registerCallback(call: MethodCall, result: MethodChannel.Result) {
val callbackHandle = call.argument<Number>("callbackHandle")?.toLong() val callbackHandle = call.argument<Number>("callbackHandle")?.toLong()
if (callbackHandle == null) { if (callbackHandle == null) {
result.error("registerCallback-args", "failed because of missing arguments", null) result.error("registerCallback-args", "missing arguments", null)
return return
} }

View file

@ -0,0 +1,32 @@
package deckers.thibault.aves.channel.calls
import android.appwidget.AppWidgetManager
import android.content.Context
import deckers.thibault.aves.HomeWidgetProvider
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class HomeWidgetHandler(private val context: Context) : MethodChannel.MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"update" -> Coresult.safe(call, result, ::update)
else -> result.notImplemented()
}
}
private fun update(call: MethodCall, result: MethodChannel.Result) {
val widgetId = call.argument<Int>("widgetId")
if (widgetId == null) {
result.error("update-args", "missing arguments", null)
return
}
val appWidgetManager = AppWidgetManager.getInstance(context)
HomeWidgetProvider().onUpdate(context, appWidgetManager, intArrayOf(widgetId))
result.success(null)
}
companion object {
const val CHANNEL = "deckers.thibault/aves/widget_update"
}
}

View file

@ -0,0 +1,77 @@
package deckers.thibault.aves.channel.calls
import android.content.ContextWrapper
import android.net.Uri
import android.util.Log
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.NameConflictStrategy
import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class MediaEditHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"cancelFileOp" -> safe(call, result, ::cancelFileOp)
"captureFrame" -> ioScope.launch { safeSuspend(call, result, ::captureFrame) }
else -> result.notImplemented()
}
}
private fun cancelFileOp(call: MethodCall, result: MethodChannel.Result) {
val opId = call.argument<String>("opId")
if (opId == null) {
result.error("cancelFileOp-args", "missing arguments", null)
return
}
Log.i(LOG_TAG, "cancelling file op $opId")
cancelledOps.add(opId)
result.success(null)
}
private suspend fun captureFrame(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val desiredName = call.argument<String>("desiredName")
val exifFields = call.argument<FieldMap>("exif") ?: HashMap()
val bytes = call.argument<ByteArray>("bytes")
var destinationDir = call.argument<String>("destinationPath")
val nameConflictStrategy = NameConflictStrategy.get(call.argument<String>("nameConflictStrategy"))
if (uri == null || desiredName == null || bytes == null || destinationDir == null || nameConflictStrategy == null) {
result.error("captureFrame-args", "missing arguments", null)
return
}
val provider = getProvider(uri)
if (provider == null) {
result.error("captureFrame-provider", "failed to find provider for uri=$uri", null)
return
}
destinationDir = ensureTrailingSeparator(destinationDir)
provider.captureFrame(contextWrapper, desiredName, exifFields, bytes, destinationDir, nameConflictStrategy, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("captureFrame-failure", "failed to capture frame for uri=$uri", throwable.message)
})
}
companion object {
private val LOG_TAG = LogUtils.createTag<MediaEditHandler>()
const val CHANNEL = "deckers.thibault/aves/media_edit"
val cancelledOps = HashSet<String>()
}
}

View file

@ -1,9 +1,8 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.app.Activity import android.content.Context
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.util.Log
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
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
@ -12,31 +11,29 @@ import deckers.thibault.aves.channel.calls.fetchers.SvgRegionFetcher
import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher
import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.NameConflictStrategy
import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlin.math.roundToInt import kotlin.math.roundToInt
class MediaFileHandler(private val activity: Activity) : MethodCallHandler { class MediaFetchHandler(private val context: Context) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val density = activity.resources.displayMetrics.density private val density = context.resources.displayMetrics.density
private val regionFetcher = RegionFetcher(activity) private val regionFetcher = RegionFetcher(context)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getEntry" -> ioScope.launch { safe(call, result, ::getEntry) } "getEntry" -> ioScope.launch { safe(call, result, ::getEntry) }
"getThumbnail" -> ioScope.launch { safeSuspend(call, result, ::getThumbnail) } "getThumbnail" -> ioScope.launch { safeSuspend(call, result, ::getThumbnail) }
"getRegion" -> ioScope.launch { safeSuspend(call, result, ::getRegion) } "getRegion" -> ioScope.launch { safeSuspend(call, result, ::getRegion) }
"cancelFileOp" -> safe(call, result, ::cancelFileOp)
"captureFrame" -> ioScope.launch { safeSuspend(call, result, ::captureFrame) }
"clearSizedThumbnailDiskCache" -> ioScope.launch { safe(call, result, ::clearSizedThumbnailDiskCache) } "clearSizedThumbnailDiskCache" -> ioScope.launch { safe(call, result, ::clearSizedThumbnailDiskCache) }
else -> result.notImplemented() else -> result.notImplemented()
} }
@ -46,7 +43,7 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
val mimeType = call.argument<String>("mimeType") // MIME type is optional val mimeType = call.argument<String>("mimeType") // MIME type is optional
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (uri == null) { if (uri == null) {
result.error("getEntry-args", "failed because of missing arguments", null) result.error("getEntry-args", "missing arguments", null)
return return
} }
@ -56,7 +53,7 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
return return
} }
provider.fetchSingle(activity, uri, mimeType, object : ImageOpCallback { provider.fetchSingle(context, uri, mimeType, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri", throwable.message) override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri", throwable.message)
}) })
@ -74,13 +71,13 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
val defaultSizeDip = call.argument<Number>("defaultSizeDip")?.toDouble() val defaultSizeDip = call.argument<Number>("defaultSizeDip")?.toDouble()
if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) { if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) {
result.error("getThumbnail-args", "failed because of missing arguments", null) result.error("getThumbnail-args", "missing arguments", null)
return return
} }
// convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter // convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter
ThumbnailFetcher( ThumbnailFetcher(
activity, context,
uri, uri,
mimeType, mimeType,
dateModifiedSecs, dateModifiedSecs,
@ -107,20 +104,20 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
val imageHeight = call.argument<Int>("imageHeight") val imageHeight = call.argument<Int>("imageHeight")
if (uri == null || mimeType == null || sampleSize == null || x == null || y == null || width == null || height == null || imageWidth == null || imageHeight == null) { if (uri == null || mimeType == null || sampleSize == null || x == null || y == null || width == null || height == null || imageWidth == null || imageHeight == null) {
result.error("getRegion-args", "failed because of missing arguments", null) result.error("getRegion-args", "missing arguments", null)
return return
} }
val regionRect = Rect(x, y, x + width, y + height) val regionRect = Rect(x, y, x + width, y + height)
when (mimeType) { when (mimeType) {
MimeTypes.SVG -> SvgRegionFetcher(activity).fetch( MimeTypes.SVG -> SvgRegionFetcher(context).fetch(
uri = uri, uri = uri,
regionRect = regionRect, regionRect = regionRect,
imageWidth = imageWidth, imageWidth = imageWidth,
imageHeight = imageHeight, imageHeight = imageHeight,
result = result, result = result,
) )
MimeTypes.TIFF -> TiffRegionFetcher(activity).fetch( MimeTypes.TIFF -> TiffRegionFetcher(context).fetch(
uri = uri, uri = uri,
page = pageId ?: 0, page = pageId ?: 0,
sampleSize = sampleSize, sampleSize = sampleSize,
@ -140,53 +137,12 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
} }
} }
private fun cancelFileOp(call: MethodCall, result: MethodChannel.Result) {
val opId = call.argument<String>("opId")
if (opId == null) {
result.error("cancelFileOp-args", "failed because of missing arguments", null)
return
}
Log.i(LOG_TAG, "cancelling file op $opId")
cancelledOps.add(opId)
result.success(null)
}
private suspend fun captureFrame(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val desiredName = call.argument<String>("desiredName")
val exifFields = call.argument<FieldMap>("exif") ?: HashMap()
val bytes = call.argument<ByteArray>("bytes")
var destinationDir = call.argument<String>("destinationPath")
val nameConflictStrategy = NameConflictStrategy.get(call.argument<String>("nameConflictStrategy"))
if (uri == null || desiredName == null || bytes == null || destinationDir == null || nameConflictStrategy == null) {
result.error("captureFrame-args", "failed because of missing arguments", null)
return
}
val provider = getProvider(uri)
if (provider == null) {
result.error("captureFrame-provider", "failed to find provider for uri=$uri", null)
return
}
destinationDir = ensureTrailingSeparator(destinationDir)
provider.captureFrame(activity, desiredName, exifFields, bytes, destinationDir, nameConflictStrategy, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("captureFrame-failure", "failed to capture frame for uri=$uri", throwable.message)
})
}
private fun clearSizedThumbnailDiskCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun clearSizedThumbnailDiskCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
Glide.get(activity).clearDiskCache() Glide.get(context).clearDiskCache()
result.success(null) result.success(null)
} }
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<MediaFileHandler>() const val CHANNEL = "deckers.thibault/aves/media_fetch"
const val CHANNEL = "deckers.thibault/aves/media_file"
val cancelledOps = HashSet<String>()
} }
} }

View file

@ -28,7 +28,7 @@ class MediaStoreHandler(private val context: Context) : MethodCallHandler {
private fun checkObsoleteContentIds(call: MethodCall, result: MethodChannel.Result) { private fun checkObsoleteContentIds(call: MethodCall, result: MethodChannel.Result) {
val knownContentIds = call.argument<List<Int?>>("knownContentIds") val knownContentIds = call.argument<List<Int?>>("knownContentIds")
if (knownContentIds == null) { if (knownContentIds == null) {
result.error("checkObsoleteContentIds-args", "failed because of missing arguments", null) result.error("checkObsoleteContentIds-args", "missing arguments", null)
return return
} }
result.success(MediaStoreImageProvider().checkObsoleteContentIds(context, knownContentIds)) result.success(MediaStoreImageProvider().checkObsoleteContentIds(context, knownContentIds))
@ -37,7 +37,7 @@ class MediaStoreHandler(private val context: Context) : MethodCallHandler {
private fun checkObsoletePaths(call: MethodCall, result: MethodChannel.Result) { private fun checkObsoletePaths(call: MethodCall, result: MethodChannel.Result) {
val knownPathById = call.argument<Map<Int?, String?>>("knownPathById") val knownPathById = call.argument<Map<Int?, String?>>("knownPathById")
if (knownPathById == null) { if (knownPathById == null) {
result.error("checkObsoletePaths-args", "failed because of missing arguments", null) result.error("checkObsoletePaths-args", "missing arguments", null)
return return
} }
result.success(MediaStoreImageProvider().checkObsoletePaths(context, knownPathById)) result.success(MediaStoreImageProvider().checkObsoletePaths(context, knownPathById))

View file

@ -1,6 +1,6 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.app.Activity import android.content.ContextWrapper
import android.net.Uri import android.net.Uri
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.model.ExifOrientationOp import deckers.thibault.aves.model.ExifOrientationOp
@ -15,7 +15,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MetadataEditHandler(private val activity: Activity) : MethodCallHandler { class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
@ -33,7 +33,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
private fun rotate(call: MethodCall, result: MethodChannel.Result) { private fun rotate(call: MethodCall, result: MethodChannel.Result) {
val clockwise = call.argument<Boolean>("clockwise") val clockwise = call.argument<Boolean>("clockwise")
if (clockwise == null) { if (clockwise == null) {
result.error("rotate-args", "failed because of missing arguments", null) result.error("rotate-args", "missing arguments", null)
return return
} }
@ -48,7 +48,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
private fun editOrientation(call: MethodCall, result: MethodChannel.Result, op: ExifOrientationOp) { private fun editOrientation(call: MethodCall, result: MethodChannel.Result, op: ExifOrientationOp) {
val entryMap = call.argument<FieldMap>("entry") val entryMap = call.argument<FieldMap>("entry")
if (entryMap == null) { if (entryMap == null) {
result.error("editOrientation-args", "failed because of missing arguments", null) result.error("editOrientation-args", "missing arguments", null)
return return
} }
@ -66,7 +66,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
return return
} }
provider.editOrientation(activity, path, uri, mimeType, op, object : ImageOpCallback { provider.editOrientation(contextWrapper, path, uri, mimeType, op, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("editOrientation-failure", "failed to change orientation for mimeType=$mimeType uri=$uri", throwable.message) override fun onFailure(throwable: Throwable) = result.error("editOrientation-failure", "failed to change orientation for mimeType=$mimeType uri=$uri", throwable.message)
}) })
@ -78,7 +78,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
val fields = call.argument<List<String>>("fields") val fields = call.argument<List<String>>("fields")
val entryMap = call.argument<FieldMap>("entry") val entryMap = call.argument<FieldMap>("entry")
if (entryMap == null || fields == null) { if (entryMap == null || fields == null) {
result.error("editDate-args", "failed because of missing arguments", null) result.error("editDate-args", "missing arguments", null)
return return
} }
@ -96,7 +96,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
return return
} }
provider.editDate(activity, path, uri, mimeType, dateMillis, shiftMinutes, fields, object : ImageOpCallback { provider.editDate(contextWrapper, path, uri, mimeType, dateMillis, shiftMinutes, fields, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("editDate-failure", "failed to edit date for mimeType=$mimeType uri=$uri", throwable.message) override fun onFailure(throwable: Throwable) = result.error("editDate-failure", "failed to edit date for mimeType=$mimeType uri=$uri", throwable.message)
}) })
@ -107,7 +107,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
val entryMap = call.argument<FieldMap>("entry") val entryMap = call.argument<FieldMap>("entry")
val autoCorrectTrailerOffset = call.argument<Boolean>("autoCorrectTrailerOffset") val autoCorrectTrailerOffset = call.argument<Boolean>("autoCorrectTrailerOffset")
if (entryMap == null || metadata == null || autoCorrectTrailerOffset == null) { if (entryMap == null || metadata == null || autoCorrectTrailerOffset == null) {
result.error("editMetadata-args", "failed because of missing arguments", null) result.error("editMetadata-args", "missing arguments", null)
return return
} }
@ -125,7 +125,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
return return
} }
provider.editMetadata(activity, path, uri, mimeType, metadata, autoCorrectTrailerOffset, callback = object : ImageOpCallback { provider.editMetadata(contextWrapper, path, uri, mimeType, metadata, autoCorrectTrailerOffset, callback = object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("editMetadata-failure", "failed to edit metadata for mimeType=$mimeType uri=$uri", throwable.message) override fun onFailure(throwable: Throwable) = result.error("editMetadata-failure", "failed to edit metadata for mimeType=$mimeType uri=$uri", throwable.message)
}) })
@ -134,7 +134,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
private fun removeTrailerVideo(call: MethodCall, result: MethodChannel.Result) { private fun removeTrailerVideo(call: MethodCall, result: MethodChannel.Result) {
val entryMap = call.argument<FieldMap>("entry") val entryMap = call.argument<FieldMap>("entry")
if (entryMap == null) { if (entryMap == null) {
result.error("removeTrailerVideo-args", "failed because of missing arguments", null) result.error("removeTrailerVideo-args", "missing arguments", null)
return return
} }
@ -152,7 +152,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
return return
} }
provider.removeTrailerVideo(activity, path, uri, mimeType, object : ImageOpCallback { provider.removeTrailerVideo(contextWrapper, path, uri, mimeType, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("removeTrailerVideo-failure", "failed to remove trailer video for mimeType=$mimeType uri=$uri", throwable.message) override fun onFailure(throwable: Throwable) = result.error("removeTrailerVideo-failure", "failed to remove trailer video for mimeType=$mimeType uri=$uri", throwable.message)
}) })
@ -162,7 +162,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
val types = call.argument<List<String>>("types") val types = call.argument<List<String>>("types")
val entryMap = call.argument<FieldMap>("entry") val entryMap = call.argument<FieldMap>("entry")
if (entryMap == null || types == null) { if (entryMap == null || types == null) {
result.error("removeTypes-args", "failed because of missing arguments", null) result.error("removeTypes-args", "missing arguments", null)
return return
} }
@ -180,7 +180,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
return return
} }
provider.removeMetadataTypes(activity, path, uri, mimeType, types.toSet(), object : ImageOpCallback { provider.removeMetadataTypes(contextWrapper, path, uri, mimeType, types.toSet(), object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("removeTypes-failure", "failed to remove metadata for mimeType=$mimeType uri=$uri", throwable.message) override fun onFailure(throwable: Throwable) = result.error("removeTypes-failure", "failed to remove metadata for mimeType=$mimeType uri=$uri", throwable.message)
}) })

View file

@ -115,7 +115,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getAllMetadata-args", "failed because of missing arguments", null) result.error("getAllMetadata-args", "missing arguments", null)
return return
} }
@ -424,7 +424,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val path = call.argument<String>("path") val path = call.argument<String>("path")
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getCatalogMetadata-args", "failed because of missing arguments", null) result.error("getCatalogMetadata-args", "missing arguments", null)
return return
} }
@ -691,7 +691,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getOverlayMetadata-args", "failed because of missing arguments", null) result.error("getOverlayMetadata-args", "missing arguments", null)
return return
} }
@ -761,7 +761,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getGeoTiffInfo-args", "failed because of missing arguments", null) result.error("getGeoTiffInfo-args", "missing arguments", null)
return return
} }
@ -802,7 +802,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null || sizeBytes == null) { if (mimeType == null || uri == null || sizeBytes == null) {
result.error("getMultiPageInfo-args", "failed because of missing arguments", null) result.error("getMultiPageInfo-args", "missing arguments", null)
return return
} }
@ -824,7 +824,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getPanoramaInfo-args", "failed because of missing arguments", null) result.error("getPanoramaInfo-args", "missing arguments", null)
return return
} }
@ -863,7 +863,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getIptc-args", "failed because of missing arguments", null) result.error("getIptc-args", "missing arguments", null)
return return
} }
@ -888,7 +888,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getXmp-args", "failed because of missing arguments", null) result.error("getXmp-args", "missing arguments", null)
return return
} }
@ -918,7 +918,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun hasContentResolverProp(call: MethodCall, result: MethodChannel.Result) { private fun hasContentResolverProp(call: MethodCall, result: MethodChannel.Result) {
val prop = call.argument<String>("prop") val prop = call.argument<String>("prop")
if (prop == null) { if (prop == null) {
result.error("hasContentResolverProp-args", "failed because of missing arguments", null) result.error("hasContentResolverProp-args", "missing arguments", null)
return return
} }
@ -938,7 +938,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val prop = call.argument<String>("prop") val prop = call.argument<String>("prop")
if (mimeType == null || uri == null || prop == null) { if (mimeType == null || uri == null || prop == null) {
result.error("getContentResolverProp-args", "failed because of missing arguments", null) result.error("getContentResolverProp-args", "missing arguments", null)
return return
} }
@ -992,7 +992,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
val field = call.argument<String>("field") val field = call.argument<String>("field")
if (mimeType == null || uri == null || field == null) { if (mimeType == null || uri == null || field == null) {
result.error("getDate-args", "failed because of missing arguments", null) result.error("getDate-args", "missing arguments", null)
return return
} }

View file

@ -55,7 +55,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
) )
) )
} }
} catch (e: IllegalArgumentException) { } catch (e: Exception) {
// ignore // ignore
} }
} }
@ -80,7 +80,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
"state" to EnvironmentCompat.getStorageState(volumeFile) "state" to EnvironmentCompat.getStorageState(volumeFile)
) )
) )
} catch (e: IllegalArgumentException) { } catch (e: Exception) {
// ignore // ignore
} }
} }
@ -91,7 +91,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
private fun getFreeSpace(call: MethodCall, result: MethodChannel.Result) { private fun getFreeSpace(call: MethodCall, result: MethodChannel.Result) {
val path = call.argument<String>("path") val path = call.argument<String>("path")
if (path == null) { if (path == null) {
result.error("getFreeSpace-args", "failed because of missing arguments", null) result.error("getFreeSpace-args", "missing arguments", null)
return return
} }
@ -112,7 +112,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
private fun getInaccessibleDirectories(call: MethodCall, result: MethodChannel.Result) { private fun getInaccessibleDirectories(call: MethodCall, result: MethodChannel.Result) {
val dirPaths = call.argument<List<String>>("dirPaths") val dirPaths = call.argument<List<String>>("dirPaths")
if (dirPaths == null) { if (dirPaths == null) {
result.error("getInaccessibleDirectories-args", "failed because of missing arguments", null) result.error("getInaccessibleDirectories-args", "missing arguments", null)
return return
} }
@ -126,7 +126,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
private fun revokeDirectoryAccess(call: MethodCall, result: MethodChannel.Result) { private fun revokeDirectoryAccess(call: MethodCall, result: MethodChannel.Result) {
val path = call.argument<String>("path") val path = call.argument<String>("path")
if (path == null) { if (path == null) {
result.error("revokeDirectoryAccess-args", "failed because of missing arguments", null) result.error("revokeDirectoryAccess-args", "missing arguments", null)
return return
} }
@ -142,7 +142,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
private fun deleteEmptyDirectories(call: MethodCall, result: MethodChannel.Result) { private fun deleteEmptyDirectories(call: MethodCall, result: MethodChannel.Result) {
val dirPaths = call.argument<List<String>>("dirPaths") val dirPaths = call.argument<List<String>>("dirPaths")
if (dirPaths == null) { if (dirPaths == null) {
result.error("deleteEmptyDirectories-args", "failed because of missing arguments", null) result.error("deleteEmptyDirectories-args", "missing arguments", null)
return return
} }
@ -167,7 +167,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
private fun canInsertMedia(call: MethodCall, result: MethodChannel.Result) { private fun canInsertMedia(call: MethodCall, result: MethodChannel.Result) {
val directories = call.argument<List<FieldMap>>("directories") val directories = call.argument<List<FieldMap>>("directories")
if (directories == null) { if (directories == null) {
result.error("canInsertMedia-args", "failed because of missing arguments", null) result.error("canInsertMedia-args", "missing arguments", null)
return return
} }

View file

@ -1,9 +1,9 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.app.Activity
import android.app.WallpaperManager import android.app.WallpaperManager
import android.app.WallpaperManager.FLAG_LOCK import android.app.WallpaperManager.FLAG_LOCK
import android.app.WallpaperManager.FLAG_SYSTEM import android.app.WallpaperManager.FLAG_SYSTEM
import android.content.ContextWrapper
import android.os.Build import android.os.Build
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@ -14,7 +14,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class WallpaperHandler(private val activity: Activity) : MethodCallHandler { class WallpaperHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
@ -29,11 +29,11 @@ class WallpaperHandler(private val activity: Activity) : MethodCallHandler {
val home = call.argument<Boolean>("home") val home = call.argument<Boolean>("home")
val lock = call.argument<Boolean>("lock") val lock = call.argument<Boolean>("lock")
if (bytes == null || home == null || lock == null) { if (bytes == null || home == null || lock == null) {
result.error("setWallpaper-args", "failed because of missing arguments", null) result.error("setWallpaper-args", "missing arguments", null)
return return
} }
val manager = WallpaperManager.getInstance(activity) val manager = WallpaperManager.getInstance(contextWrapper)
val supported = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || manager.isWallpaperSupported val supported = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || manager.isWallpaperSupported
val allowed = Build.VERSION.SDK_INT < Build.VERSION_CODES.N || manager.isSetWallpaperAllowed val allowed = Build.VERSION.SDK_INT < Build.VERSION_CODES.N || manager.isSetWallpaperAllowed
if (!supported || !allowed) { if (!supported || !allowed) {

View file

@ -1,89 +0,0 @@
package deckers.thibault.aves.channel.calls
import android.app.Activity
import android.os.Build
import android.provider.Settings
import android.util.Log
import android.view.WindowManager
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
class WindowHandler(private val activity: Activity) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"keepScreenOn" -> safe(call, result, ::keepScreenOn)
"isRotationLocked" -> safe(call, result, ::isRotationLocked)
"requestOrientation" -> safe(call, result, ::requestOrientation)
"canSetCutoutMode" -> safe(call, result, ::canSetCutoutMode)
"setCutoutMode" -> safe(call, result, ::setCutoutMode)
else -> result.notImplemented()
}
}
private fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) {
val on = call.argument<Boolean>("on")
if (on == null) {
result.error("keepOn-args", "failed because of missing arguments", null)
return
}
val window = activity.window
val flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
if (on) {
window.addFlags(flag)
} else {
window.clearFlags(flag)
}
result.success(null)
}
private fun isRotationLocked(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
var locked = false
try {
locked = Settings.System.getInt(activity.contentResolver, Settings.System.ACCELEROMETER_ROTATION) == 0
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
}
result.success(locked)
}
private fun requestOrientation(call: MethodCall, result: MethodChannel.Result) {
val orientation = call.argument<Int>("orientation")
if (orientation == null) {
result.error("requestOrientation-args", "failed because of missing arguments", null)
return
}
activity.requestedOrientation = orientation
result.success(true)
}
private fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
}
private fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
val use = call.argument<Boolean>("use")
if (use == null) {
result.error("setCutoutMode-args", "failed because of missing arguments", null)
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val mode = if (use) {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
} else {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
activity.window.attributes.layoutInDisplayCutoutMode = mode
}
result.success(true)
}
companion object {
private val LOG_TAG = LogUtils.createTag<WindowHandler>()
const val CHANNEL = "deckers.thibault/aves/window"
}
}

View file

@ -0,0 +1,67 @@
package deckers.thibault.aves.channel.calls.window
import android.app.Activity
import android.os.Build
import android.view.WindowManager
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class ActivityWindowHandler(private val activity: Activity) : WindowHandler(activity) {
override fun isActivity(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(true)
}
override fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) {
val on = call.argument<Boolean>("on")
if (on == null) {
result.error("keepOn-args", "missing arguments", null)
return
}
val window = activity.window
val flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
if (on) {
window.addFlags(flag)
} else {
window.clearFlags(flag)
}
result.success(null)
}
override fun requestOrientation(call: MethodCall, result: MethodChannel.Result) {
val orientation = call.argument<Int>("orientation")
if (orientation == null) {
result.error("requestOrientation-args", "missing arguments", null)
return
}
activity.requestedOrientation = orientation
result.success(true)
}
override fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
}
override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
val use = call.argument<Boolean>("use")
if (use == null) {
result.error("setCutoutMode-args", "missing arguments", null)
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val mode = if (use) {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
} else {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
activity.window.attributes.layoutInDisplayCutoutMode = mode
}
result.success(true)
}
companion object {
private val LOG_TAG = LogUtils.createTag<ActivityWindowHandler>()
}
}

View file

@ -0,0 +1,27 @@
package deckers.thibault.aves.channel.calls.window
import android.app.Service
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class ServiceWindowHandler(service: Service) : WindowHandler(service) {
override fun isActivity(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
override fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) {
result.success(null)
}
override fun requestOrientation(call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
override fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
}

View file

@ -0,0 +1,48 @@
package deckers.thibault.aves.channel.calls.window
import android.content.ContextWrapper
import android.provider.Settings
import android.util.Log
import deckers.thibault.aves.channel.calls.Coresult
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
abstract class WindowHandler(private val contextWrapper: ContextWrapper) : MethodChannel.MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"isActivity" -> Coresult.safe(call, result, ::isActivity)
"keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn)
"isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked)
"requestOrientation" -> Coresult.safe(call, result, ::requestOrientation)
"canSetCutoutMode" -> Coresult.safe(call, result, ::canSetCutoutMode)
"setCutoutMode" -> Coresult.safe(call, result, ::setCutoutMode)
else -> result.notImplemented()
}
}
abstract fun isActivity(call: MethodCall, result: MethodChannel.Result)
abstract fun keepScreenOn(call: MethodCall, result: MethodChannel.Result)
private fun isRotationLocked(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
var locked = false
try {
locked = Settings.System.getInt(contextWrapper.contentResolver, Settings.System.ACCELEROMETER_ROTATION) == 0
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
}
result.success(locked)
}
abstract fun requestOrientation(call: MethodCall, result: MethodChannel.Result)
abstract fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result)
abstract fun setCutoutMode(call: MethodCall, result: MethodChannel.Result)
companion object {
private val LOG_TAG = LogUtils.createTag<WindowHandler>()
const val CHANNEL = "deckers.thibault/aves/window"
}
}

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()
} }
} }
@ -55,7 +56,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
private suspend fun requestDirectoryAccess() { private suspend fun requestDirectoryAccess() {
val path = args["path"] as String? val path = args["path"] as String?
if (path == null) { if (path == null) {
error("requestDirectoryAccess-args", "failed because of missing arguments", null) error("requestDirectoryAccess-args", "missing arguments", null)
return return
} }
@ -77,7 +78,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
val uris = (args["uris"] as List<*>?)?.mapNotNull { if (it is String) Uri.parse(it) else null } val uris = (args["uris"] as List<*>?)?.mapNotNull { if (it is String) Uri.parse(it) else null }
val mimeTypes = (args["mimeTypes"] as List<*>?)?.mapNotNull { if (it is String) it else null } val mimeTypes = (args["mimeTypes"] as List<*>?)?.mapNotNull { if (it is String) it else null }
if (uris == null || uris.isEmpty() || mimeTypes == null || mimeTypes.size != uris.size) { if (uris == null || uris.isEmpty() || mimeTypes == null || mimeTypes.size != uris.size) {
error("requestMediaFileAccess-args", "failed because of missing arguments", null) error("requestMediaFileAccess-args", "missing arguments", null)
return return
} }
@ -111,7 +112,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
val mimeType = args["mimeType"] as String? val mimeType = args["mimeType"] as String?
val bytes = args["bytes"] as ByteArray? val bytes = args["bytes"] as ByteArray?
if (name == null || mimeType == null || bytes == null) { if (name == null || mimeType == null || bytes == null) {
error("createFile-args", "failed because of missing arguments", null) error("createFile-args", "missing arguments", null)
return return
} }
@ -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

@ -87,7 +87,7 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
val pageId = arguments["pageId"] as Int? val pageId = arguments["pageId"] as Int?
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
error("streamImage-args", "failed because of missing arguments", null) error("streamImage-args", "missing arguments", null)
endOfStream() endOfStream()
return return
} }

View file

@ -5,7 +5,7 @@ import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import deckers.thibault.aves.channel.calls.MediaFileHandler.Companion.cancelledOps import deckers.thibault.aves.channel.calls.MediaEditHandler.Companion.cancelledOps
import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.NameConflictStrategy import deckers.thibault.aves.model.NameConflictStrategy
@ -143,7 +143,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
val height = arguments["height"] as Int? val height = arguments["height"] as Int?
val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?) val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?)
if (destinationDir == null || mimeType == null || width == null || height == null || nameConflictStrategy == null) { if (destinationDir == null || mimeType == null || width == null || height == null || nameConflictStrategy == null) {
error("export-args", "failed because of missing arguments", null) error("export-args", "missing arguments", null)
return return
} }
@ -174,7 +174,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?) val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?)
val rawEntryMap = arguments["entriesByDestination"] as Map<*, *>? val rawEntryMap = arguments["entriesByDestination"] as Map<*, *>?
if (copy == null || nameConflictStrategy == null || rawEntryMap == null || rawEntryMap.isEmpty()) { if (copy == null || nameConflictStrategy == null || rawEntryMap == null || rawEntryMap.isEmpty()) {
error("move-args", "failed because of missing arguments", null) error("move-args", "missing arguments", null)
return return
} }
@ -207,7 +207,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
val rawEntryMap = arguments["entriesToNewName"] as Map<*, *>? val rawEntryMap = arguments["entriesToNewName"] as Map<*, *>?
if (rawEntryMap == null || rawEntryMap.isEmpty()) { if (rawEntryMap == null || rawEntryMap.isEmpty()) {
error("rename-args", "failed because of missing arguments", null) error("rename-args", "missing arguments", null)
return return
} }

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,7 +1,7 @@
package deckers.thibault.aves.model.provider package deckers.thibault.aves.model.provider
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import deckers.thibault.aves.model.SourceEntry import deckers.thibault.aves.model.SourceEntry
@ -37,7 +37,7 @@ internal class FileImageProvider : ImageProvider() {
} }
} }
override suspend fun delete(activity: Activity, uri: Uri, path: String?, mimeType: String) { override suspend fun delete(contextWrapper: ContextWrapper, uri: Uri, path: String?, mimeType: String) {
val file = File(File(uri.path!!).path) val file = File(File(uri.path!!).path)
if (!file.exists()) return if (!file.exists()) return

View file

@ -2,6 +2,7 @@ package deckers.thibault.aves.model.provider
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
@ -45,7 +46,7 @@ abstract class ImageProvider {
callback.onFailure(UnsupportedOperationException("`fetchSingle` is not supported by this image provider")) callback.onFailure(UnsupportedOperationException("`fetchSingle` is not supported by this image provider"))
} }
open suspend fun delete(activity: Activity, uri: Uri, path: String?, mimeType: String) { open suspend fun delete(contextWrapper: ContextWrapper, uri: Uri, path: String?, mimeType: String) {
throw UnsupportedOperationException("`delete` is not supported by this image provider") throw UnsupportedOperationException("`delete` is not supported by this image provider")
} }
@ -151,7 +152,7 @@ abstract class ImageProvider {
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}" desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
} }
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
activity = activity, contextWrapper = activity,
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = exportMimeType, mimeType = exportMimeType,
@ -242,7 +243,7 @@ abstract class ImageProvider {
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
suspend fun captureFrame( suspend fun captureFrame(
activity: Activity, contextWrapper: ContextWrapper,
desiredNameWithoutExtension: String, desiredNameWithoutExtension: String,
exifFields: FieldMap, exifFields: FieldMap,
bytes: ByteArray, bytes: ByteArray,
@ -250,7 +251,7 @@ abstract class ImageProvider {
nameConflictStrategy: NameConflictStrategy, nameConflictStrategy: NameConflictStrategy,
callback: ImageOpCallback, callback: ImageOpCallback,
) { ) {
val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir) val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(contextWrapper, targetDir)
if (!File(targetDir).exists()) { if (!File(targetDir).exists()) {
callback.onFailure(Exception("failed to create directory at path=$targetDir")) callback.onFailure(Exception("failed to create directory at path=$targetDir"))
return return
@ -265,7 +266,7 @@ abstract class ImageProvider {
val captureMimeType = MimeTypes.JPEG val captureMimeType = MimeTypes.JPEG
val targetNameWithoutExtension = try { val targetNameWithoutExtension = try {
resolveTargetFileNameWithoutExtension( resolveTargetFileNameWithoutExtension(
activity = activity, contextWrapper = contextWrapper,
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = captureMimeType, mimeType = captureMimeType,
@ -287,7 +288,7 @@ abstract class ImageProvider {
// through a document URI, not a tree URI // through a document URI, not a tree URI
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
val targetTreeFile = targetDirDocFile.createFile(captureMimeType, targetNameWithoutExtension) val targetTreeFile = targetDirDocFile.createFile(captureMimeType, targetNameWithoutExtension)
val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri) val targetDocFile = DocumentFileCompat.fromSingleUri(contextWrapper, targetTreeFile.uri)
try { try {
if (exifFields.isEmpty()) { if (exifFields.isEmpty()) {
@ -355,7 +356,7 @@ abstract class ImageProvider {
val fileName = targetDocFile.name val fileName = targetDocFile.name
val targetFullPath = targetDir + fileName val targetFullPath = targetDir + fileName
val newFields = MediaStoreImageProvider().scanNewPath(activity, targetFullPath, captureMimeType) val newFields = MediaStoreImageProvider().scanNewPath(contextWrapper, targetFullPath, captureMimeType)
callback.onSuccess(newFields) callback.onSuccess(newFields)
} catch (e: Exception) { } catch (e: Exception) {
callback.onFailure(e) callback.onFailure(e)
@ -364,7 +365,7 @@ abstract class ImageProvider {
// returns available name to use, or `null` to skip it // returns available name to use, or `null` to skip it
suspend fun resolveTargetFileNameWithoutExtension( suspend fun resolveTargetFileNameWithoutExtension(
activity: Activity, contextWrapper: ContextWrapper,
dir: String, dir: String,
desiredNameWithoutExtension: String, desiredNameWithoutExtension: String,
mimeType: String, mimeType: String,
@ -386,9 +387,9 @@ abstract class ImageProvider {
if (targetFile.exists()) { if (targetFile.exists()) {
val path = targetFile.path val path = targetFile.path
MediaStoreImageProvider().apply { MediaStoreImageProvider().apply {
val uri = getContentUriForPath(activity, path) val uri = getContentUriForPath(contextWrapper, path)
uri ?: throw Exception("failed to find content URI for path=$path") uri ?: throw Exception("failed to find content URI for path=$path")
delete(activity, uri, path, mimeType) delete(contextWrapper, uri, path, mimeType)
} }
} }
desiredNameWithoutExtension desiredNameWithoutExtension

View file

@ -3,10 +3,7 @@ package deckers.thibault.aves.model.provider
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.RecoverableSecurityException import android.app.RecoverableSecurityException
import android.content.ContentResolver import android.content.*
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -280,7 +277,7 @@ class MediaStoreImageProvider : ImageProvider() {
private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType
// `uri` is a media URI, not a document URI // `uri` is a media URI, not a document URI
override suspend fun delete(activity: Activity, uri: Uri, path: String?, mimeType: String) { override suspend fun delete(contextWrapper: ContextWrapper, uri: Uri, path: String?, mimeType: String) {
path ?: throw Exception("failed to delete file because path is null") path ?: throw Exception("failed to delete file because path is null")
// the following situations are possible: // the following situations are possible:
@ -291,10 +288,10 @@ class MediaStoreImageProvider : ImageProvider() {
val fileExists = file.exists() val fileExists = file.exists()
if (fileExists) { if (fileExists) {
if (StorageUtils.canEditByFile(activity, path)) { if (StorageUtils.canEditByFile(contextWrapper, path)) {
if (hasEntry(activity, uri)) { if (hasEntry(contextWrapper, uri)) {
Log.d(LOG_TAG, "delete [permission:file, file exists, content exists] content at uri=$uri path=$path") Log.d(LOG_TAG, "delete [permission:file, file exists, content exists] content at uri=$uri path=$path")
activity.contentResolver.delete(uri, null, null) contextWrapper.contentResolver.delete(uri, null, null)
} }
// in theory, deleting via content resolver should remove the file on storage // in theory, deleting via content resolver should remove the file on storage
// in practice, the file may still be there afterwards // in practice, the file may still be there afterwards
@ -303,31 +300,31 @@ class MediaStoreImageProvider : ImageProvider() {
if (file.delete()) { if (file.delete()) {
// in theory, scanning an obsolete path should remove the entry from the Media Store // in theory, scanning an obsolete path should remove the entry from the Media Store
// in practice, the entry may still be there afterwards // in practice, the entry may still be there afterwards
scanObsoletePath(activity, uri, path, mimeType) scanObsoletePath(contextWrapper, uri, path, mimeType)
return return
} }
} else { } else {
return return
} }
} else if (!isMediaUriPermissionGranted(activity, uri, mimeType) } else if (!isMediaUriPermissionGranted(contextWrapper, uri, mimeType)
&& StorageUtils.requireAccessPermission(activity, path) && StorageUtils.requireAccessPermission(contextWrapper, path)
) { ) {
// the delete request may yield a `RecoverableSecurityException` when using scoped storage, // the delete request may yield a `RecoverableSecurityException` when using scoped storage,
// even if we have permissions on the tree document via SAF // even if we have permissions on the tree document via SAF
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && hasEntry(activity, uri)) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && hasEntry(contextWrapper, uri)) {
Log.d(LOG_TAG, "delete [permission:doc, file exists, content exists] content at uri=$uri path=$path") Log.d(LOG_TAG, "delete [permission:doc, file exists, content exists] content at uri=$uri path=$path")
activity.contentResolver.delete(uri, null, null) contextWrapper.contentResolver.delete(uri, null, null)
} }
// in theory, deleting via content resolver should remove the file on storage // in theory, deleting via content resolver should remove the file on storage
// in practice, the file may still be there afterwards // in practice, the file may still be there afterwards
if (file.exists()) { if (file.exists()) {
Log.d(LOG_TAG, "delete [permission:doc, file exists after content delete] document at uri=$uri path=$path") Log.d(LOG_TAG, "delete [permission:doc, file exists after content delete] document at uri=$uri path=$path")
val df = StorageUtils.getDocumentFile(activity, path, uri) val df = StorageUtils.getDocumentFile(contextWrapper, path, uri)
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
if (df != null && df.delete()) { if (df != null && df.delete()) {
scanObsoletePath(activity, uri, path, mimeType) scanObsoletePath(contextWrapper, uri, path, mimeType)
return return
} }
throw Exception("failed to delete document with df=$df") throw Exception("failed to delete document with df=$df")
@ -343,28 +340,28 @@ class MediaStoreImageProvider : ImageProvider() {
try { try {
Log.d(LOG_TAG, "delete [file exists=$fileExists] content at uri=$uri path=$path") Log.d(LOG_TAG, "delete [file exists=$fileExists] content at uri=$uri path=$path")
if (activity.contentResolver.delete(uri, null, null) > 0) return if (contextWrapper.contentResolver.delete(uri, null, null) > 0) return
if (hasEntry(activity, uri) || file.exists()) { if (hasEntry(contextWrapper, uri) || file.exists()) {
throw Exception("failed to delete row from content provider") throw Exception("failed to delete row from content provider")
} }
} catch (securityException: SecurityException) { } catch (securityException: SecurityException) {
// even if the app has access permission granted on the containing directory, // even if the app has access permission granted on the containing directory,
// the delete request may yield a `RecoverableSecurityException` on Android 10+ // the delete request may yield a `RecoverableSecurityException` on Android 10+
// when the underlying file no longer exists and this is an orphaned entry in the Media Store // when the underlying file no longer exists and this is an orphaned entry in the Media Store
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && contextWrapper is Activity) {
Log.w(LOG_TAG, "caught a security exception when attempting to delete uri=$uri", securityException) Log.w(LOG_TAG, "caught a security exception when attempting to delete uri=$uri", securityException)
val rse = securityException as? RecoverableSecurityException ?: throw securityException val rse = securityException as? RecoverableSecurityException ?: throw securityException
val intentSender = rse.userAction.actionIntent.intentSender val intentSender = rse.userAction.actionIntent.intentSender
// request user permission for this item // request user permission for this item
MainActivity.pendingScopedStoragePermissionCompleter = CompletableFuture<Boolean>() MainActivity.pendingScopedStoragePermissionCompleter = CompletableFuture<Boolean>()
activity.startIntentSenderForResult(intentSender, DELETE_SINGLE_PERMISSION_REQUEST, null, 0, 0, 0, null) contextWrapper.startIntentSenderForResult(intentSender, DELETE_SINGLE_PERMISSION_REQUEST, null, 0, 0, 0, null)
val granted = MainActivity.pendingScopedStoragePermissionCompleter!!.join() val granted = MainActivity.pendingScopedStoragePermissionCompleter!!.join()
MainActivity.pendingScopedStoragePermissionCompleter = null MainActivity.pendingScopedStoragePermissionCompleter = null
if (granted) { if (granted) {
delete(activity, uri, path, mimeType) delete(contextWrapper, uri, path, mimeType)
} else { } else {
throw Exception("failed to get delete permission") throw Exception("failed to get delete permission")
} }
@ -494,7 +491,7 @@ class MediaStoreImageProvider : ImageProvider() {
val desiredNameWithoutExtension = desiredName.substringBeforeLast(".") val desiredNameWithoutExtension = desiredName.substringBeforeLast(".")
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
activity = activity, contextWrapper = activity,
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType, mimeType = mimeType,
@ -641,7 +638,7 @@ class MediaStoreImageProvider : ImageProvider() {
val dir = oldFile.parent ?: return skippedFieldMap val dir = oldFile.parent ?: return skippedFieldMap
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
activity = activity, contextWrapper = activity,
dir = dir, dir = dir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType, mimeType = mimeType,

View file

@ -24,7 +24,6 @@ fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): Ap
@Suppress("deprecation") @Suppress("deprecation")
getApplicationInfo(packageName, flags) getApplicationInfo(packageName, flags)
} }
} }
fun PackageManager.queryIntentActivitiesCompat(intent: Intent, flags: Int): List<ResolveInfo> { fun PackageManager.queryIntentActivitiesCompat(intent: Intent, flags: Int): List<ResolveInfo> {

View file

@ -290,7 +290,7 @@ object StorageUtils {
if (volume != null && uuid.equals(volume.uuid, ignoreCase = true)) { if (volume != null && uuid.equals(volume.uuid, ignoreCase = true)) {
return volumePath return volumePath
} }
} catch (e: IllegalArgumentException) { } catch (e: Exception) {
// ignore // ignore
} }
} }

View file

@ -0,0 +1,5 @@
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_img"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter" />

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Aves</string> <string name="app_name">Aves</string>
<string name="app_widget_label">Bilderrahmen</string>
<string name="wallpaper">Hintergrundbild</string> <string name="wallpaper">Hintergrundbild</string>
<string name="search_shortcut_short_label">Suche</string> <string name="search_shortcut_short_label">Suche</string>
<string name="videos_shortcut_short_label">Videos</string> <string name="videos_shortcut_short_label">Videos</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Aves</string> <string name="app_name">Aves</string>
<string name="app_widget_label">Cadre photo</string>
<string name="wallpaper">Fond décran</string> <string name="wallpaper">Fond décran</string>
<string name="search_shortcut_short_label">Recherche</string> <string name="search_shortcut_short_label">Recherche</string>
<string name="videos_shortcut_short_label">Vidéos</string> <string name="videos_shortcut_short_label">Vidéos</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Aves</string> <string name="app_name">Aves</string>
<string name="app_widget_label">Cornice foto</string>
<string name="wallpaper">Sfondo</string> <string name="wallpaper">Sfondo</string>
<string name="search_shortcut_short_label">Ricerca</string> <string name="search_shortcut_short_label">Ricerca</string>
<string name="videos_shortcut_short_label">Video</string> <string name="videos_shortcut_short_label">Video</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Aves</string> <string name="app_name">Aves</string>
<string name="app_widget_label">フォトフレーム</string>
<string name="wallpaper">壁紙</string> <string name="wallpaper">壁紙</string>
<string name="search_shortcut_short_label">検索</string> <string name="search_shortcut_short_label">検索</string>
<string name="videos_shortcut_short_label">動画</string> <string name="videos_shortcut_short_label">動画</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">아베스</string> <string name="app_name">아베스</string>
<string name="app_widget_label">사진 액자</string>
<string name="wallpaper">배경화면</string> <string name="wallpaper">배경화면</string>
<string name="search_shortcut_short_label">검색</string> <string name="search_shortcut_short_label">검색</string>
<string name="videos_shortcut_short_label">동영상</string> <string name="videos_shortcut_short_label">동영상</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Aves</string> <string name="app_name">Aves</string>
<string name="app_widget_label">Porta-retratos</string>
<string name="wallpaper">Papel de parede</string> <string name="wallpaper">Papel de parede</string>
<string name="search_shortcut_short_label">Procurar</string> <string name="search_shortcut_short_label">Procurar</string>
<string name="videos_shortcut_short_label">Vídeos</string> <string name="videos_shortcut_short_label">Vídeos</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Aves</string> <string name="app_name">Aves</string>
<string name="app_widget_label">相框</string>
<string name="wallpaper">壁纸</string> <string name="wallpaper">壁纸</string>
<string name="search_shortcut_short_label">搜索</string> <string name="search_shortcut_short_label">搜索</string>
<string name="videos_shortcut_short_label">视频</string> <string name="videos_shortcut_short_label">视频</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Aves</string> <string name="app_name">Aves</string>
<string name="app_widget_label">Photo Frame</string>
<string name="wallpaper">Wallpaper</string> <string name="wallpaper">Wallpaper</string>
<string name="search_shortcut_short_label">Search</string> <string name="search_shortcut_short_label">Search</string>
<string name="videos_shortcut_short_label">Videos</string> <string name="videos_shortcut_short_label">Videos</string>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="deckers.thibault.aves.HomeWidgetSettingsActivity"
android:initialLayout="@layout/app_widget"
android:minWidth="40dp"
android:minHeight="40dp"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="2"
android:targetCellHeight="2"
android:updatePeriodMillis="3600000"
android:widgetCategory="home_screen"
android:widgetFeatures="reconfigurable" />

View file

@ -0,0 +1,2 @@
<dream xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="deckers.thibault.aves/deckers.thibault.aves.ScreenSaverSettingsActivity" />

View file

@ -0,0 +1,2 @@
<dream xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="deckers.thibault.aves.profile/deckers.thibault.aves.ScreenSaverSettingsActivity" />

View file

@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.7.0' ext.kotlin_version = '1.7.10'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
@ -10,8 +10,8 @@ buildscript {
classpath 'com.android.tools.build:gradle:7.2.1' classpath 'com.android.tools.build:gradle:7.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// GMS & Firebase Crashlytics (used by some flavors only) // GMS & Firebase Crashlytics (used by some flavors only)
classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.gms:google-services:4.3.13'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.0' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.1'
// HMS (used by some flavors only) // HMS (used by some flavors only)
classpath 'com.huawei.agconnect:agcp:1.5.2.300' classpath 'com.huawei.agconnect:agcp:1.5.2.300'
} }

View file

@ -3,3 +3,4 @@ storePassword=<KEYSTORE_PASSWORD>
keyAlias=<KEY_ALIAS> keyAlias=<KEY_ALIAS>
keyPassword=<KEY_PASSWORD> keyPassword=<KEY_PASSWORD>
googleApiKey=<GOOGLE_API_KEY> googleApiKey=<GOOGLE_API_KEY>
huaweiApiKey=<HUAWEI_API_KEY>

View file

@ -0,0 +1,5 @@
In v1.6.10:
- add the photo frame widget to your home
- use your photos as screen saver
- search photos taken "on this day"
Full changelog available on GitHub

View file

@ -2,4 +2,4 @@
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc. <b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
<i>Aves</i> integrates with Android (from <b>API 19 to 33</b>, i.e. from KitKat to Android 13) with features such as <b>app shortcuts</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>. <i>Aves</i> integrates with Android (from <b>API 19 to 33</b>, i.e. from KitKat to Android 13) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.

View file

@ -1,22 +1,34 @@
enum AppMode { enum AppMode {
main, main,
pickCollectionFiltersExternal,
pickSingleMediaExternal, pickSingleMediaExternal,
pickMultipleMediaExternal, pickMultipleMediaExternal,
pickMediaInternal, pickMediaInternal,
pickFilterInternal, pickFilterInternal,
screenSaver,
setWallpaper, setWallpaper,
slideshow, slideshow,
view, view,
} }
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

@ -1,4 +1,4 @@
import 'dart:ui' as ui show Codec; import 'dart:ui' as ui;
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
@ -27,7 +27,7 @@ class AppIconImage extends ImageProvider<AppIconImageKey> {
} }
@override @override
ImageStreamCompleter load(AppIconImageKey key, DecoderCallback decode) { ImageStreamCompleter loadBuffer(AppIconImageKey key, DecoderBufferCallback decode) {
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode), codec: _loadAsync(key, decode),
scale: key.scale, scale: key.scale,
@ -37,10 +37,11 @@ class AppIconImage extends ImageProvider<AppIconImageKey> {
); );
} }
Future<ui.Codec> _loadAsync(AppIconImageKey key, DecoderCallback decode) async { Future<ui.Codec> _loadAsync(AppIconImageKey key, DecoderBufferCallback decode) async {
try { try {
final bytes = await androidAppService.getAppIcon(key.packageName, key.size); final bytes = await androidAppService.getAppIcon(key.packageName, key.size);
return await decode(bytes.isEmpty ? kTransparentImage : bytes); final buffer = await ui.ImmutableBuffer.fromUint8List(bytes.isEmpty ? kTransparentImage : bytes);
return await decode(buffer);
} catch (error) { } catch (error) {
debugPrint('$runtimeType _loadAsync failed with packageName=$packageName, error=$error'); debugPrint('$runtimeType _loadAsync failed with packageName=$packageName, error=$error');
throw StateError('$packageName app icon decoding failed'); throw StateError('$packageName app icon decoding failed');

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui show Codec; import 'dart:ui' as ui;
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
@ -18,7 +18,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
} }
@override @override
ImageStreamCompleter load(RegionProviderKey key, DecoderCallback decode) { ImageStreamCompleter loadBuffer(RegionProviderKey key, DecoderBufferCallback decode) {
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode), codec: _loadAsync(key, decode),
scale: 1.0, scale: 1.0,
@ -28,12 +28,12 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
); );
} }
Future<ui.Codec> _loadAsync(RegionProviderKey key, DecoderCallback decode) async { Future<ui.Codec> _loadAsync(RegionProviderKey key, DecoderBufferCallback decode) async {
final uri = key.uri; final uri = key.uri;
final mimeType = key.mimeType; final mimeType = key.mimeType;
final pageId = key.pageId; final pageId = key.pageId;
try { try {
final bytes = await mediaFileService.getRegion( final bytes = await mediaFetchService.getRegion(
uri, uri,
mimeType, mimeType,
key.rotationDegrees, key.rotationDegrees,
@ -47,7 +47,8 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
if (bytes.isEmpty) { if (bytes.isEmpty) {
throw StateError('$uri ($mimeType) region loading failed'); throw StateError('$uri ($mimeType) region loading failed');
} }
return await decode(bytes); final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
return await decode(buffer);
} catch (error) { } catch (error) {
// loading may fail if the provided MIME type is incorrect (e.g. the Media Store may report a JPEG as a TIFF) // loading may fail if the provided MIME type is incorrect (e.g. the Media Store may report a JPEG as a TIFF)
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error'); debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
@ -57,11 +58,11 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
@override @override
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, RegionProviderKey key, ImageErrorListener handleError) { void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, RegionProviderKey key, ImageErrorListener handleError) {
mediaFileService.resumeLoading(key); mediaFetchService.resumeLoading(key);
super.resolveStreamForKey(configuration, stream, key, handleError); super.resolveStreamForKey(configuration, stream, key, handleError);
} }
void pause() => mediaFileService.cancelRegion(key); void pause() => mediaFetchService.cancelRegion(key);
} }
@immutable @immutable

View file

@ -1,4 +1,4 @@
import 'dart:ui' as ui show Codec; import 'dart:ui' as ui;
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
@ -19,7 +19,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
} }
@override @override
ImageStreamCompleter load(ThumbnailProviderKey key, DecoderCallback decode) { ImageStreamCompleter loadBuffer(ThumbnailProviderKey key, DecoderBufferCallback decode) {
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode), codec: _loadAsync(key, decode),
scale: 1.0, scale: 1.0,
@ -30,12 +30,12 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
); );
} }
Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async { Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, DecoderBufferCallback decode) async {
final uri = key.uri; final uri = key.uri;
final mimeType = key.mimeType; final mimeType = key.mimeType;
final pageId = key.pageId; final pageId = key.pageId;
try { try {
final bytes = await mediaFileService.getThumbnail( final bytes = await mediaFetchService.getThumbnail(
uri: uri, uri: uri,
mimeType: mimeType, mimeType: mimeType,
pageId: pageId, pageId: pageId,
@ -48,7 +48,8 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
if (bytes.isEmpty) { if (bytes.isEmpty) {
throw StateError('$uri ($mimeType) loading failed'); throw StateError('$uri ($mimeType) loading failed');
} }
return await decode(bytes); final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
return await decode(buffer);
} catch (error) { } catch (error) {
// loading may fail if the provided MIME type is incorrect (e.g. the Media Store may report a JPEG as a TIFF) // loading may fail if the provided MIME type is incorrect (e.g. the Media Store may report a JPEG as a TIFF)
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error'); debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
@ -58,11 +59,11 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
@override @override
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, ImageErrorListener handleError) { void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, ImageErrorListener handleError) {
mediaFileService.resumeLoading(key); mediaFetchService.resumeLoading(key);
super.resolveStreamForKey(configuration, stream, key, handleError); super.resolveStreamForKey(configuration, stream, key, handleError);
} }
void pause() => mediaFileService.cancelThumbnail(key); void pause() => mediaFetchService.cancelThumbnail(key);
} }
@immutable @immutable

View file

@ -1,5 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui show Codec; import 'dart:ui' as ui;
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
@ -32,7 +32,7 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
} }
@override @override
ImageStreamCompleter load(UriImage key, DecoderCallback decode) { ImageStreamCompleter loadBuffer(UriImage key, DecoderBufferCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>(); final chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
@ -45,11 +45,11 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
); );
} }
Future<ui.Codec> _loadAsync(UriImage key, DecoderCallback decode, StreamController<ImageChunkEvent> chunkEvents) async { Future<ui.Codec> _loadAsync(UriImage key, DecoderBufferCallback decode, StreamController<ImageChunkEvent> chunkEvents) async {
assert(key == this); assert(key == this);
try { try {
final bytes = await mediaFileService.getImage( final bytes = await mediaFetchService.getImage(
uri, uri,
mimeType, mimeType,
rotationDegrees, rotationDegrees,
@ -66,7 +66,8 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
if (bytes.isEmpty) { if (bytes.isEmpty) {
throw StateError('$uri ($mimeType) loading failed'); throw StateError('$uri ($mimeType) loading failed');
} }
return await decode(bytes); final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
return await decode(buffer);
} catch (error) { } catch (error) {
// loading may fail if the provided MIME type is incorrect (e.g. the Media Store may report a JPEG as a TIFF) // loading may fail if the provided MIME type is incorrect (e.g. the Media Store may report a JPEG as a TIFF)
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error'); debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');

View file

@ -25,7 +25,8 @@
"showTooltip": "Anzeigen", "showTooltip": "Anzeigen",
"hideTooltip": "Ausblenden", "hideTooltip": "Ausblenden",
"actionRemove": "Entfernen", "actionRemove": "Entfernen",
"resetButtonTooltip": "Zurücksetzen", "resetTooltip": "Zurücksetzen",
"saveTooltip": "Speichern",
"doubleBackExitMessage": "Zum Verlassen erneut auf „Zurück“ tippen.", "doubleBackExitMessage": "Zum Verlassen erneut auf „Zurück“ tippen.",
"doNotAskAgain": "Nicht noch einmal fragen", "doNotAskAgain": "Nicht noch einmal fragen",
@ -94,6 +95,7 @@
"filterFavouriteLabel": "Favorit", "filterFavouriteLabel": "Favorit",
"filterLocationEmptyLabel": "Ungeortet", "filterLocationEmptyLabel": "Ungeortet",
"filterTagEmptyLabel": "Unmarkiert", "filterTagEmptyLabel": "Unmarkiert",
"filterOnThisDayLabel": "Am heutigen Tag",
"filterRatingUnratedLabel": "Nicht bewertet", "filterRatingUnratedLabel": "Nicht bewertet",
"filterRatingRejectedLabel": "Verworfen", "filterRatingRejectedLabel": "Verworfen",
"filterTypeAnimatedLabel": "Animationen", "filterTypeAnimatedLabel": "Animationen",
@ -303,7 +305,6 @@
"aboutBug": "Fehlerbericht", "aboutBug": "Fehlerbericht",
"aboutBugSaveLogInstruction": "Anwendungsprotokolle in einer Datei speichern", "aboutBugSaveLogInstruction": "Anwendungsprotokolle in einer Datei speichern",
"aboutBugSaveLogButton": "Speichern",
"aboutBugCopyInfoInstruction": "Systeminformationen kopieren", "aboutBugCopyInfoInstruction": "Systeminformationen kopieren",
"aboutBugCopyInfoButton": "Kopieren", "aboutBugCopyInfoButton": "Kopieren",
"aboutBugReportInstruction": "Bericht auf GitHub mit den Protokollen und Systeminformationen", "aboutBugReportInstruction": "Bericht auf GitHub mit den Protokollen und Systeminformationen",
@ -417,6 +418,7 @@
"searchCollectionFieldHint": "Sammlung durchsuchen", "searchCollectionFieldHint": "Sammlung durchsuchen",
"searchSectionRecent": "Neueste", "searchSectionRecent": "Neueste",
"searchSectionDate": "Datum",
"searchSectionAlbums": "Alben", "searchSectionAlbums": "Alben",
"searchSectionCountries": "Länder", "searchSectionCountries": "Länder",
"searchSectionPlaces": "Orte", "searchSectionPlaces": "Orte",
@ -502,6 +504,7 @@
"settingsViewerSlideshowTitle": "Diashow", "settingsViewerSlideshowTitle": "Diashow",
"settingsSlideshowRepeat": "Wiederholung", "settingsSlideshowRepeat": "Wiederholung",
"settingsSlideshowShuffle": "Mischen", "settingsSlideshowShuffle": "Mischen",
"settingsSlideshowFillScreen": "Bildschirm ausfüllen",
"settingsSlideshowTransitionTile": "Übergang", "settingsSlideshowTransitionTile": "Übergang",
"settingsSlideshowTransitionTitle": "Übergang", "settingsSlideshowTransitionTitle": "Übergang",
"settingsSlideshowIntervalTile": "Intervall", "settingsSlideshowIntervalTile": "Intervall",
@ -584,6 +587,11 @@
"settingsUnitSystemTile": "Einheiten", "settingsUnitSystemTile": "Einheiten",
"settingsUnitSystemTitle": "Einheiten", "settingsUnitSystemTitle": "Einheiten",
"settingsScreenSaverPageTitle": "Bildschirmschoner",
"settingsWidgetPageTitle": "Bilderrahmen",
"settingsWidgetShowOutline": "Gliederung",
"statsPageTitle": "Statistiken", "statsPageTitle": "Statistiken",
"statsWithGps": "{count, plural, =1{1 Element mit Standort} other{{count} Elemente mit Standort}}", "statsWithGps": "{count, plural, =1{1 Element mit Standort} other{{count} Elemente mit Standort}}",
"statsTopCountries": "Top-Länder", "statsTopCountries": "Top-Länder",

View file

@ -53,7 +53,8 @@
"showTooltip": "Show", "showTooltip": "Show",
"hideTooltip": "Hide", "hideTooltip": "Hide",
"actionRemove": "Remove", "actionRemove": "Remove",
"resetButtonTooltip": "Reset", "resetTooltip": "Reset",
"saveTooltip": "Save",
"doubleBackExitMessage": "Tap “back” again to exit.", "doubleBackExitMessage": "Tap “back” again to exit.",
"doNotAskAgain": "Do not ask again", "doNotAskAgain": "Do not ask again",
@ -122,6 +123,7 @@
"filterFavouriteLabel": "Favorite", "filterFavouriteLabel": "Favorite",
"filterLocationEmptyLabel": "Unlocated", "filterLocationEmptyLabel": "Unlocated",
"filterTagEmptyLabel": "Untagged", "filterTagEmptyLabel": "Untagged",
"filterOnThisDayLabel": "On this day",
"filterRatingUnratedLabel": "Unrated", "filterRatingUnratedLabel": "Unrated",
"filterRatingRejectedLabel": "Rejected", "filterRatingRejectedLabel": "Rejected",
"filterTypeAnimatedLabel": "Animated", "filterTypeAnimatedLabel": "Animated",
@ -433,7 +435,6 @@
"aboutBug": "Bug Report", "aboutBug": "Bug Report",
"aboutBugSaveLogInstruction": "Save app logs to a file", "aboutBugSaveLogInstruction": "Save app logs to a file",
"aboutBugSaveLogButton": "Save",
"aboutBugCopyInfoInstruction": "Copy system information", "aboutBugCopyInfoInstruction": "Copy system information",
"aboutBugCopyInfoButton": "Copy", "aboutBugCopyInfoButton": "Copy",
"aboutBugReportInstruction": "Report on GitHub with the logs and system information", "aboutBugReportInstruction": "Report on GitHub with the logs and system information",
@ -597,6 +598,7 @@
"searchCollectionFieldHint": "Search collection", "searchCollectionFieldHint": "Search collection",
"searchSectionRecent": "Recent", "searchSectionRecent": "Recent",
"searchSectionDate": "Date",
"searchSectionAlbums": "Albums", "searchSectionAlbums": "Albums",
"searchSectionCountries": "Countries", "searchSectionCountries": "Countries",
"searchSectionPlaces": "Places", "searchSectionPlaces": "Places",
@ -682,6 +684,7 @@
"settingsViewerSlideshowTitle": "Slideshow", "settingsViewerSlideshowTitle": "Slideshow",
"settingsSlideshowRepeat": "Repeat", "settingsSlideshowRepeat": "Repeat",
"settingsSlideshowShuffle": "Shuffle", "settingsSlideshowShuffle": "Shuffle",
"settingsSlideshowFillScreen": "Fill screen",
"settingsSlideshowTransitionTile": "Transition", "settingsSlideshowTransitionTile": "Transition",
"settingsSlideshowTransitionTitle": "Transition", "settingsSlideshowTransitionTitle": "Transition",
"settingsSlideshowIntervalTile": "Interval", "settingsSlideshowIntervalTile": "Interval",
@ -764,6 +767,11 @@
"settingsUnitSystemTile": "Units", "settingsUnitSystemTile": "Units",
"settingsUnitSystemTitle": "Units", "settingsUnitSystemTitle": "Units",
"settingsScreenSaverPageTitle": "Screen Saver",
"settingsWidgetPageTitle": "Photo Frame",
"settingsWidgetShowOutline": "Outline",
"statsPageTitle": "Stats", "statsPageTitle": "Stats",
"statsWithGps": "{count, plural, =1{1 item with location} other{{count} items with location}}", "statsWithGps": "{count, plural, =1{1 item with location} other{{count} items with location}}",
"@statsWithGps": { "@statsWithGps": {

View file

@ -25,7 +25,8 @@
"showTooltip": "Mostrar", "showTooltip": "Mostrar",
"hideTooltip": "Ocultar", "hideTooltip": "Ocultar",
"actionRemove": "Remover", "actionRemove": "Remover",
"resetButtonTooltip": "Restablecer", "resetTooltip": "Restablecer",
"saveTooltip": "Guardar",
"doubleBackExitMessage": "Presione «atrás» nuevamente para salir.", "doubleBackExitMessage": "Presione «atrás» nuevamente para salir.",
"doNotAskAgain": "No preguntar nuevamente", "doNotAskAgain": "No preguntar nuevamente",
@ -303,7 +304,6 @@
"aboutBug": "Reporte de errores", "aboutBug": "Reporte de errores",
"aboutBugSaveLogInstruction": "Guardar registros de la aplicación a un archivo", "aboutBugSaveLogInstruction": "Guardar registros de la aplicación a un archivo",
"aboutBugSaveLogButton": "Guardar",
"aboutBugCopyInfoInstruction": "Copiar información del sistema", "aboutBugCopyInfoInstruction": "Copiar información del sistema",
"aboutBugCopyInfoButton": "Copiar", "aboutBugCopyInfoButton": "Copiar",
"aboutBugReportInstruction": "Reportar en GitHub con los registros y la información del sistema", "aboutBugReportInstruction": "Reportar en GitHub con los registros y la información del sistema",
@ -417,6 +417,7 @@
"searchCollectionFieldHint": "Buscar en colección", "searchCollectionFieldHint": "Buscar en colección",
"searchSectionRecent": "Reciente", "searchSectionRecent": "Reciente",
"searchSectionDate": "Fecha",
"searchSectionAlbums": "Álbumes", "searchSectionAlbums": "Álbumes",
"searchSectionCountries": "Países", "searchSectionCountries": "Países",
"searchSectionPlaces": "Lugares", "searchSectionPlaces": "Lugares",

View file

@ -25,7 +25,8 @@
"showTooltip": "Afficher", "showTooltip": "Afficher",
"hideTooltip": "Masquer", "hideTooltip": "Masquer",
"actionRemove": "Supprimer", "actionRemove": "Supprimer",
"resetButtonTooltip": "Réinitialiser", "resetTooltip": "Réinitialiser",
"saveTooltip": "Sauvegarder",
"doubleBackExitMessage": "Pressez «\u00A0retour\u00A0» à nouveau pour quitter.", "doubleBackExitMessage": "Pressez «\u00A0retour\u00A0» à nouveau pour quitter.",
"doNotAskAgain": "Ne pas demander de nouveau", "doNotAskAgain": "Ne pas demander de nouveau",
@ -94,6 +95,7 @@
"filterFavouriteLabel": "Favori", "filterFavouriteLabel": "Favori",
"filterLocationEmptyLabel": "Sans lieu", "filterLocationEmptyLabel": "Sans lieu",
"filterTagEmptyLabel": "Sans libellé", "filterTagEmptyLabel": "Sans libellé",
"filterOnThisDayLabel": "Ce jour-là",
"filterRatingUnratedLabel": "Sans notation", "filterRatingUnratedLabel": "Sans notation",
"filterRatingRejectedLabel": "Rejeté", "filterRatingRejectedLabel": "Rejeté",
"filterTypeAnimatedLabel": "Animation", "filterTypeAnimatedLabel": "Animation",
@ -303,7 +305,6 @@
"aboutBug": "Rapports derreur", "aboutBug": "Rapports derreur",
"aboutBugSaveLogInstruction": "Sauvegarder les logs de lapp vers un fichier", "aboutBugSaveLogInstruction": "Sauvegarder les logs de lapp vers un fichier",
"aboutBugSaveLogButton": "Sauvegarder",
"aboutBugCopyInfoInstruction": "Copier les informations denvironnement", "aboutBugCopyInfoInstruction": "Copier les informations denvironnement",
"aboutBugCopyInfoButton": "Copier", "aboutBugCopyInfoButton": "Copier",
"aboutBugReportInstruction": "Créer une «\u00A0issue\u00A0» sur GitHub en attachant les logs et informations denvironnement", "aboutBugReportInstruction": "Créer une «\u00A0issue\u00A0» sur GitHub en attachant les logs et informations denvironnement",
@ -417,6 +418,7 @@
"searchCollectionFieldHint": "Recherche", "searchCollectionFieldHint": "Recherche",
"searchSectionRecent": "Historique", "searchSectionRecent": "Historique",
"searchSectionDate": "Date",
"searchSectionAlbums": "Albums", "searchSectionAlbums": "Albums",
"searchSectionCountries": "Pays", "searchSectionCountries": "Pays",
"searchSectionPlaces": "Lieux", "searchSectionPlaces": "Lieux",
@ -502,6 +504,7 @@
"settingsViewerSlideshowTitle": "Diaporama", "settingsViewerSlideshowTitle": "Diaporama",
"settingsSlideshowRepeat": "Répéter", "settingsSlideshowRepeat": "Répéter",
"settingsSlideshowShuffle": "Aléatoire", "settingsSlideshowShuffle": "Aléatoire",
"settingsSlideshowFillScreen": "Remplir lécran",
"settingsSlideshowTransitionTile": "Transition", "settingsSlideshowTransitionTile": "Transition",
"settingsSlideshowTransitionTitle": "Transition", "settingsSlideshowTransitionTitle": "Transition",
"settingsSlideshowIntervalTile": "Intervalle", "settingsSlideshowIntervalTile": "Intervalle",
@ -574,7 +577,7 @@
"settingsThemeBrightness": "Thème", "settingsThemeBrightness": "Thème",
"settingsThemeColorHighlights": "Surlignages colorés", "settingsThemeColorHighlights": "Surlignages colorés",
"settingsThemeEnableDynamicColor": "Couleur dynamique", "settingsThemeEnableDynamicColor": "Couleur dynamique",
"settingsDisplayRefreshRateModeTile": "Fréquence dactualisation de l'écran", "settingsDisplayRefreshRateModeTile": "Fréquence dactualisation de lécran",
"settingsDisplayRefreshRateModeTitle": "Fréquence dactualisation", "settingsDisplayRefreshRateModeTitle": "Fréquence dactualisation",
"settingsSectionLanguage": "Langue & Formats", "settingsSectionLanguage": "Langue & Formats",
@ -584,6 +587,11 @@
"settingsUnitSystemTile": "Unités", "settingsUnitSystemTile": "Unités",
"settingsUnitSystemTitle": "Unités", "settingsUnitSystemTitle": "Unités",
"settingsScreenSaverPageTitle": "Économiseur décran",
"settingsWidgetPageTitle": "Cadre photo",
"settingsWidgetShowOutline": "Contours",
"statsPageTitle": "Statistiques", "statsPageTitle": "Statistiques",
"statsWithGps": "{count, plural, =1{1 élément localisé} other{{count} éléments localisés}}", "statsWithGps": "{count, plural, =1{1 élément localisé} other{{count} éléments localisés}}",
"statsTopCountries": "Top pays", "statsTopCountries": "Top pays",

View file

@ -25,7 +25,8 @@
"showTooltip": "Tampilkan", "showTooltip": "Tampilkan",
"hideTooltip": "Sembunyikan", "hideTooltip": "Sembunyikan",
"actionRemove": "Hapus", "actionRemove": "Hapus",
"resetButtonTooltip": "Ulang", "resetTooltip": "Ulang",
"saveTooltip": "Simpan",
"doubleBackExitMessage": "Ketuk “kembali” lagi untuk keluar.", "doubleBackExitMessage": "Ketuk “kembali” lagi untuk keluar.",
"doNotAskAgain": "Jangan tanya lagi", "doNotAskAgain": "Jangan tanya lagi",
@ -81,6 +82,9 @@
"videoActionSetSpeed": "Kecepatan pemutaran", "videoActionSetSpeed": "Kecepatan pemutaran",
"videoActionSettings": "Pengaturan", "videoActionSettings": "Pengaturan",
"slideshowActionResume": "Lanjutkan",
"slideshowActionShowInCollection": "Tampilkan di Koleksi",
"entryInfoActionEditDate": "Ubah tanggal & waktu", "entryInfoActionEditDate": "Ubah tanggal & waktu",
"entryInfoActionEditLocation": "Ubah lokasi", "entryInfoActionEditLocation": "Ubah lokasi",
"entryInfoActionEditRating": "Ubah nilai", "entryInfoActionEditRating": "Ubah nilai",
@ -145,10 +149,23 @@
"displayRefreshRatePreferHighest": "Penyegaran tertinggi", "displayRefreshRatePreferHighest": "Penyegaran tertinggi",
"displayRefreshRatePreferLowest": "Penyegaran terendah", "displayRefreshRatePreferLowest": "Penyegaran terendah",
"slideshowVideoPlaybackSkip": "Lewati",
"slideshowVideoPlaybackMuted": "Mainkan bisu",
"slideshowVideoPlaybackWithSound": "Mainkan dengan suara",
"themeBrightnessLight": "Terang", "themeBrightnessLight": "Terang",
"themeBrightnessDark": "Gelap", "themeBrightnessDark": "Gelap",
"themeBrightnessBlack": "Hitam", "themeBrightnessBlack": "Hitam",
"viewerTransitionSlide": "Menggeser",
"viewerTransitionParallax": "Paralaks",
"viewerTransitionFade": "Memudar",
"viewerTransitionZoomIn": "Membesar",
"wallpaperTargetHome": "Tampilan depan",
"wallpaperTargetLock": "Tampilan kunci",
"wallpaperTargetHomeLock": "Tampilan depan dan kunci",
"albumTierNew": "Baru", "albumTierNew": "Baru",
"albumTierPinned": "Disemat", "albumTierPinned": "Disemat",
"albumTierSpecial": "Biasa", "albumTierSpecial": "Biasa",
@ -263,6 +280,7 @@
"menuActionSelectAll": "Pilih semua", "menuActionSelectAll": "Pilih semua",
"menuActionSelectNone": "Pilih tidak ada", "menuActionSelectNone": "Pilih tidak ada",
"menuActionMap": "Peta", "menuActionMap": "Peta",
"menuActionSlideshow": "Tampilan slide",
"menuActionStats": "Statistik", "menuActionStats": "Statistik",
"viewDialogTabSort": "Sortir", "viewDialogTabSort": "Sortir",
@ -286,7 +304,6 @@
"aboutBug": "Lapor Bug", "aboutBug": "Lapor Bug",
"aboutBugSaveLogInstruction": "Simpan log aplikasi ke file", "aboutBugSaveLogInstruction": "Simpan log aplikasi ke file",
"aboutBugSaveLogButton": "Simpan",
"aboutBugCopyInfoInstruction": "Salin informasi sistem", "aboutBugCopyInfoInstruction": "Salin informasi sistem",
"aboutBugCopyInfoButton": "Salin", "aboutBugCopyInfoButton": "Salin",
"aboutBugReportInstruction": "Laporkan ke GitHub dengan log dan informasi sistem", "aboutBugReportInstruction": "Laporkan ke GitHub dengan log dan informasi sistem",
@ -350,6 +367,7 @@
"collectionEmptyFavourites": "Tidak ada favorit", "collectionEmptyFavourites": "Tidak ada favorit",
"collectionEmptyVideos": "Tidak ada video", "collectionEmptyVideos": "Tidak ada video",
"collectionEmptyImages": "Tidak ada gambar", "collectionEmptyImages": "Tidak ada gambar",
"collectionEmptyGrantAccessButtonLabel": "Berikan akses",
"collectionSelectSectionTooltip": "Pilih bagian", "collectionSelectSectionTooltip": "Pilih bagian",
"collectionDeselectSectionTooltip": "Batalkan pilihan bagian", "collectionDeselectSectionTooltip": "Batalkan pilihan bagian",
@ -399,6 +417,7 @@
"searchCollectionFieldHint": "Cari koleksi", "searchCollectionFieldHint": "Cari koleksi",
"searchSectionRecent": "Terkini", "searchSectionRecent": "Terkini",
"searchSectionDate": "Tanggal",
"searchSectionAlbums": "Album", "searchSectionAlbums": "Album",
"searchSectionCountries": "Negara", "searchSectionCountries": "Negara",
"searchSectionPlaces": "Tempat", "searchSectionPlaces": "Tempat",
@ -480,6 +499,17 @@
"settingsViewerShowOverlayThumbnails": "Tampilkan thumbnail", "settingsViewerShowOverlayThumbnails": "Tampilkan thumbnail",
"settingsViewerEnableOverlayBlurEffect": "Efek Kabur", "settingsViewerEnableOverlayBlurEffect": "Efek Kabur",
"settingsViewerSlideshowTile": "Tampilan slide",
"settingsViewerSlideshowTitle": "Tampilan Slide",
"settingsSlideshowRepeat": "Ulangi",
"settingsSlideshowShuffle": "Acak",
"settingsSlideshowTransitionTile": "Transisi",
"settingsSlideshowTransitionTitle": "Transisi",
"settingsSlideshowIntervalTile": "Interval",
"settingsSlideshowIntervalTitle": "Interval",
"settingsSlideshowVideoPlaybackTile": "Putaran ulang video",
"settingsSlideshowVideoPlaybackTitle": "Putaran Ulang Video",
"settingsVideoPageTitle": "Pengaturan Video", "settingsVideoPageTitle": "Pengaturan Video",
"settingsSectionVideo": "Video", "settingsSectionVideo": "Video",
"settingsVideoShowVideos": "Tampilkan video", "settingsVideoShowVideos": "Tampilkan video",
@ -544,6 +574,7 @@
"settingsSectionDisplay": "Tampilan", "settingsSectionDisplay": "Tampilan",
"settingsThemeBrightness": "Tema", "settingsThemeBrightness": "Tema",
"settingsThemeColorHighlights": "Highlight warna", "settingsThemeColorHighlights": "Highlight warna",
"settingsThemeEnableDynamicColor": "Warna dinamis",
"settingsDisplayRefreshRateModeTile": "Tingkat penyegaran tampilan", "settingsDisplayRefreshRateModeTile": "Tingkat penyegaran tampilan",
"settingsDisplayRefreshRateModeTitle": "Tingkat Penyegaran", "settingsDisplayRefreshRateModeTitle": "Tingkat Penyegaran",
@ -561,6 +592,7 @@
"statsTopTags": "Label Teratas", "statsTopTags": "Label Teratas",
"viewerOpenPanoramaButtonLabel": "BUKA PANORAMA", "viewerOpenPanoramaButtonLabel": "BUKA PANORAMA",
"viewerSetWallpaperButtonLabel": "TETAPKAN SEBAGAI WALLPAPER",
"viewerErrorUnknown": "Ups!", "viewerErrorUnknown": "Ups!",
"viewerErrorDoesNotExist": "File tidak ada lagi.", "viewerErrorDoesNotExist": "File tidak ada lagi.",

View file

@ -25,7 +25,8 @@
"showTooltip": "Mostra", "showTooltip": "Mostra",
"hideTooltip": "Nascondi", "hideTooltip": "Nascondi",
"actionRemove": "Rimuovi", "actionRemove": "Rimuovi",
"resetButtonTooltip": "Reimposta", "resetTooltip": "Reimposta",
"saveTooltip": "Salva",
"doubleBackExitMessage": "Tocca di nuovo «indietro» per uscire", "doubleBackExitMessage": "Tocca di nuovo «indietro» per uscire",
"doNotAskAgain": "Non chiedere di nuovo", "doNotAskAgain": "Non chiedere di nuovo",
@ -94,6 +95,7 @@
"filterFavouriteLabel": "Preferiti", "filterFavouriteLabel": "Preferiti",
"filterLocationEmptyLabel": "Senza posizione", "filterLocationEmptyLabel": "Senza posizione",
"filterTagEmptyLabel": "Senza etichetta", "filterTagEmptyLabel": "Senza etichetta",
"filterOnThisDayLabel": "In questo giorno",
"filterRatingUnratedLabel": "Non valutato", "filterRatingUnratedLabel": "Non valutato",
"filterRatingRejectedLabel": "Rifiutato", "filterRatingRejectedLabel": "Rifiutato",
"filterTypeAnimatedLabel": "Animato", "filterTypeAnimatedLabel": "Animato",
@ -303,7 +305,6 @@
"aboutBug": "Segnalazione bug", "aboutBug": "Segnalazione bug",
"aboutBugSaveLogInstruction": "Salva i log dellapp in un file", "aboutBugSaveLogInstruction": "Salva i log dellapp in un file",
"aboutBugSaveLogButton": "Salva",
"aboutBugCopyInfoInstruction": "Copia le informazioni di sistema", "aboutBugCopyInfoInstruction": "Copia le informazioni di sistema",
"aboutBugCopyInfoButton": "Copia", "aboutBugCopyInfoButton": "Copia",
"aboutBugReportInstruction": "Segnala su GitHub con i log e le informazioni di sistema", "aboutBugReportInstruction": "Segnala su GitHub con i log e le informazioni di sistema",
@ -417,6 +418,7 @@
"searchCollectionFieldHint": "Cerca raccolta", "searchCollectionFieldHint": "Cerca raccolta",
"searchSectionRecent": "Recenti", "searchSectionRecent": "Recenti",
"searchSectionDate": "Data",
"searchSectionAlbums": "Album", "searchSectionAlbums": "Album",
"searchSectionCountries": "Paesi", "searchSectionCountries": "Paesi",
"searchSectionPlaces": "Luoghi", "searchSectionPlaces": "Luoghi",
@ -502,6 +504,7 @@
"settingsViewerSlideshowTitle": "Presentazione", "settingsViewerSlideshowTitle": "Presentazione",
"settingsSlideshowRepeat": "Ripeti", "settingsSlideshowRepeat": "Ripeti",
"settingsSlideshowShuffle": "Ordine casuale", "settingsSlideshowShuffle": "Ordine casuale",
"settingsSlideshowFillScreen": "Riempi schermo",
"settingsSlideshowTransitionTile": "Transizione", "settingsSlideshowTransitionTile": "Transizione",
"settingsSlideshowTransitionTitle": "Transizione", "settingsSlideshowTransitionTitle": "Transizione",
"settingsSlideshowIntervalTile": "Intervallo", "settingsSlideshowIntervalTile": "Intervallo",
@ -584,6 +587,11 @@
"settingsUnitSystemTile": "Unità", "settingsUnitSystemTile": "Unità",
"settingsUnitSystemTitle": "Unità", "settingsUnitSystemTitle": "Unità",
"settingsScreenSaverPageTitle": "Salvaschermo",
"settingsWidgetPageTitle": "Cornice foto",
"settingsWidgetShowOutline": "Contorno",
"statsPageTitle": "Statistiche", "statsPageTitle": "Statistiche",
"statsWithGps": "{count, plural, =1{1 elemento con posizione} other{{count} elementi con posizione}}", "statsWithGps": "{count, plural, =1{1 elemento con posizione} other{{count} elementi con posizione}}",
"statsTopCountries": "Paesi più frequenti", "statsTopCountries": "Paesi più frequenti",

View file

@ -25,7 +25,8 @@
"showTooltip": "表示する", "showTooltip": "表示する",
"hideTooltip": "非表示にする", "hideTooltip": "非表示にする",
"actionRemove": "削除", "actionRemove": "削除",
"resetButtonTooltip": "リセット", "resetTooltip": "リセット",
"saveTooltip": "保存",
"doubleBackExitMessage": "終了するには「戻る」をもう一度タップしてください。", "doubleBackExitMessage": "終了するには「戻る」をもう一度タップしてください。",
"doNotAskAgain": "今後このメッセージを表示しない", "doNotAskAgain": "今後このメッセージを表示しない",
@ -81,6 +82,9 @@
"videoActionSetSpeed": "再生速度", "videoActionSetSpeed": "再生速度",
"videoActionSettings": "設定", "videoActionSettings": "設定",
"slideshowActionResume": "再開",
"slideshowActionShowInCollection": "コレクションで表示",
"entryInfoActionEditDate": "日時を編集", "entryInfoActionEditDate": "日時を編集",
"entryInfoActionEditLocation": "位置情報を編集", "entryInfoActionEditLocation": "位置情報を編集",
"entryInfoActionEditRating": "評価を編集", "entryInfoActionEditRating": "評価を編集",
@ -91,6 +95,7 @@
"filterFavouriteLabel": "お気に入り", "filterFavouriteLabel": "お気に入り",
"filterLocationEmptyLabel": "位置情報なし", "filterLocationEmptyLabel": "位置情報なし",
"filterTagEmptyLabel": "タグ情報なし", "filterTagEmptyLabel": "タグ情報なし",
"filterOnThisDayLabel": "過去のこの日",
"filterRatingUnratedLabel": "評価情報なし", "filterRatingUnratedLabel": "評価情報なし",
"filterRatingRejectedLabel": "拒否", "filterRatingRejectedLabel": "拒否",
"filterTypeAnimatedLabel": "アニメーション", "filterTypeAnimatedLabel": "アニメーション",
@ -145,10 +150,23 @@
"displayRefreshRatePreferHighest": "高レート", "displayRefreshRatePreferHighest": "高レート",
"displayRefreshRatePreferLowest": "低レート", "displayRefreshRatePreferLowest": "低レート",
"slideshowVideoPlaybackSkip": "スキップ",
"slideshowVideoPlaybackMuted": "ミュート再生",
"slideshowVideoPlaybackWithSound": "音声あり再生",
"themeBrightnessLight": "ライト", "themeBrightnessLight": "ライト",
"themeBrightnessDark": "ダーク", "themeBrightnessDark": "ダーク",
"themeBrightnessBlack": "ブラック", "themeBrightnessBlack": "ブラック",
"viewerTransitionSlide": "スライド",
"viewerTransitionParallax": "パララックス",
"viewerTransitionFade": "フェード",
"viewerTransitionZoomIn": "ズームイン",
"wallpaperTargetHome": "ホーム画面",
"wallpaperTargetLock": "ロック画面",
"wallpaperTargetHomeLock": "ホームおよびロック画面",
"albumTierNew": "新規", "albumTierNew": "新規",
"albumTierPinned": "固定", "albumTierPinned": "固定",
"albumTierSpecial": "全体", "albumTierSpecial": "全体",
@ -263,6 +281,7 @@
"menuActionSelectAll": "すべて選択", "menuActionSelectAll": "すべて選択",
"menuActionSelectNone": "選択を解除", "menuActionSelectNone": "選択を解除",
"menuActionMap": "地図", "menuActionMap": "地図",
"menuActionSlideshow": "スライドショー",
"menuActionStats": "統計", "menuActionStats": "統計",
"viewDialogTabSort": "並べ替え", "viewDialogTabSort": "並べ替え",
@ -286,7 +305,6 @@
"aboutBug": "バグの報告", "aboutBug": "バグの報告",
"aboutBugSaveLogInstruction": "アプリのログをファイルに保存", "aboutBugSaveLogInstruction": "アプリのログをファイルに保存",
"aboutBugSaveLogButton": "保存",
"aboutBugCopyInfoInstruction": "システム情報をコピー", "aboutBugCopyInfoInstruction": "システム情報をコピー",
"aboutBugCopyInfoButton": "コピー", "aboutBugCopyInfoButton": "コピー",
"aboutBugReportInstruction": "ログとシステム情報とともに GitHub で報告", "aboutBugReportInstruction": "ログとシステム情報とともに GitHub で報告",
@ -350,6 +368,7 @@
"collectionEmptyFavourites": "お気に入りはありません", "collectionEmptyFavourites": "お気に入りはありません",
"collectionEmptyVideos": "動画はありません", "collectionEmptyVideos": "動画はありません",
"collectionEmptyImages": "画像はありません", "collectionEmptyImages": "画像はありません",
"collectionEmptyGrantAccessButtonLabel": "アクセスを許可",
"collectionSelectSectionTooltip": "セクションを選択", "collectionSelectSectionTooltip": "セクションを選択",
"collectionDeselectSectionTooltip": "セクションの選択を解除", "collectionDeselectSectionTooltip": "セクションの選択を解除",
@ -399,6 +418,7 @@
"searchCollectionFieldHint": "コレクションを検索", "searchCollectionFieldHint": "コレクションを検索",
"searchSectionRecent": "最近", "searchSectionRecent": "最近",
"searchSectionDate": "日付",
"searchSectionAlbums": "アルバム", "searchSectionAlbums": "アルバム",
"searchSectionCountries": "国", "searchSectionCountries": "国",
"searchSectionPlaces": "場所", "searchSectionPlaces": "場所",
@ -480,6 +500,18 @@
"settingsViewerShowOverlayThumbnails": "サムネイルを表示", "settingsViewerShowOverlayThumbnails": "サムネイルを表示",
"settingsViewerEnableOverlayBlurEffect": "ぼかし効果", "settingsViewerEnableOverlayBlurEffect": "ぼかし効果",
"settingsViewerSlideshowTile": "スライドショー",
"settingsViewerSlideshowTitle": "スライドショー",
"settingsSlideshowRepeat": "繰り返し",
"settingsSlideshowShuffle": "シャッフル",
"settingsSlideshowFillScreen": "画面いっぱいに表示",
"settingsSlideshowTransitionTile": "トランジション",
"settingsSlideshowTransitionTitle": "トランジション",
"settingsSlideshowIntervalTile": "間隔",
"settingsSlideshowIntervalTitle": "間隔",
"settingsSlideshowVideoPlaybackTile": "動画を再生",
"settingsSlideshowVideoPlaybackTitle": "動画再生",
"settingsVideoPageTitle": "動画設定", "settingsVideoPageTitle": "動画設定",
"settingsSectionVideo": "動画", "settingsSectionVideo": "動画",
"settingsVideoShowVideos": "動画を表示", "settingsVideoShowVideos": "動画を表示",
@ -544,6 +576,7 @@
"settingsSectionDisplay": "ディスプレイ", "settingsSectionDisplay": "ディスプレイ",
"settingsThemeBrightness": "テーマ", "settingsThemeBrightness": "テーマ",
"settingsThemeColorHighlights": "カラー強調表示", "settingsThemeColorHighlights": "カラー強調表示",
"settingsThemeEnableDynamicColor": "ダイナミックカラー",
"settingsDisplayRefreshRateModeTile": "ディスプレイ リフレッシュ レート", "settingsDisplayRefreshRateModeTile": "ディスプレイ リフレッシュ レート",
"settingsDisplayRefreshRateModeTitle": "リフレッシュ レート", "settingsDisplayRefreshRateModeTitle": "リフレッシュ レート",
@ -554,6 +587,11 @@
"settingsUnitSystemTile": "単位", "settingsUnitSystemTile": "単位",
"settingsUnitSystemTitle": "単位", "settingsUnitSystemTitle": "単位",
"settingsScreenSaverPageTitle": "スクリーンセーバー",
"settingsWidgetPageTitle": "フォトフレーム",
"settingsWidgetShowOutline": "枠",
"statsPageTitle": "統計", "statsPageTitle": "統計",
"statsWithGps": "{count, plural, other{位置情報のあるアイテム {count} 件}}", "statsWithGps": "{count, plural, other{位置情報のあるアイテム {count} 件}}",
"statsTopCountries": "上位の国", "statsTopCountries": "上位の国",
@ -561,6 +599,7 @@
"statsTopTags": "上位のタグ", "statsTopTags": "上位のタグ",
"viewerOpenPanoramaButtonLabel": "パノラマを開く", "viewerOpenPanoramaButtonLabel": "パノラマを開く",
"viewerSetWallpaperButtonLabel": "壁紙設定",
"viewerErrorUnknown": "エラー", "viewerErrorUnknown": "エラー",
"viewerErrorDoesNotExist": "ファイルが存在しません。", "viewerErrorDoesNotExist": "ファイルが存在しません。",

View file

@ -25,7 +25,8 @@
"showTooltip": "보기", "showTooltip": "보기",
"hideTooltip": "숨기기", "hideTooltip": "숨기기",
"actionRemove": "제거", "actionRemove": "제거",
"resetButtonTooltip": "복원", "resetTooltip": "복원",
"saveTooltip": "저장",
"doubleBackExitMessage": "종료하려면 한번 더 누르세요.", "doubleBackExitMessage": "종료하려면 한번 더 누르세요.",
"doNotAskAgain": "다시 묻지 않기", "doNotAskAgain": "다시 묻지 않기",
@ -94,6 +95,7 @@
"filterFavouriteLabel": "즐겨찾기", "filterFavouriteLabel": "즐겨찾기",
"filterLocationEmptyLabel": "장소 없음", "filterLocationEmptyLabel": "장소 없음",
"filterTagEmptyLabel": "태그 없음", "filterTagEmptyLabel": "태그 없음",
"filterOnThisDayLabel": "이 날",
"filterRatingUnratedLabel": "별점 없음", "filterRatingUnratedLabel": "별점 없음",
"filterRatingRejectedLabel": "거부됨", "filterRatingRejectedLabel": "거부됨",
"filterTypeAnimatedLabel": "애니메이션", "filterTypeAnimatedLabel": "애니메이션",
@ -303,7 +305,6 @@
"aboutBug": "버그 보고", "aboutBug": "버그 보고",
"aboutBugSaveLogInstruction": "앱 로그를 파일에 저장하기", "aboutBugSaveLogInstruction": "앱 로그를 파일에 저장하기",
"aboutBugSaveLogButton": "저장",
"aboutBugCopyInfoInstruction": "시스템 정보를 복사하기", "aboutBugCopyInfoInstruction": "시스템 정보를 복사하기",
"aboutBugCopyInfoButton": "복사", "aboutBugCopyInfoButton": "복사",
"aboutBugReportInstruction": "로그와 시스템 정보를 첨부하여 깃허브에서 이슈를 제출하기", "aboutBugReportInstruction": "로그와 시스템 정보를 첨부하여 깃허브에서 이슈를 제출하기",
@ -417,6 +418,7 @@
"searchCollectionFieldHint": "미디어 검색", "searchCollectionFieldHint": "미디어 검색",
"searchSectionRecent": "최근 검색기록", "searchSectionRecent": "최근 검색기록",
"searchSectionDate": "날짜",
"searchSectionAlbums": "앨범", "searchSectionAlbums": "앨범",
"searchSectionCountries": "국가", "searchSectionCountries": "국가",
"searchSectionPlaces": "장소", "searchSectionPlaces": "장소",
@ -502,6 +504,7 @@
"settingsViewerSlideshowTitle": "슬라이드쇼", "settingsViewerSlideshowTitle": "슬라이드쇼",
"settingsSlideshowRepeat": "반복", "settingsSlideshowRepeat": "반복",
"settingsSlideshowShuffle": "순서섞기", "settingsSlideshowShuffle": "순서섞기",
"settingsSlideshowFillScreen": "화면 채우기",
"settingsSlideshowTransitionTile": "전환 효과", "settingsSlideshowTransitionTile": "전환 효과",
"settingsSlideshowTransitionTitle": "전환 효과", "settingsSlideshowTransitionTitle": "전환 효과",
"settingsSlideshowIntervalTile": "교체 주기", "settingsSlideshowIntervalTile": "교체 주기",
@ -584,6 +587,11 @@
"settingsUnitSystemTile": "단위법", "settingsUnitSystemTile": "단위법",
"settingsUnitSystemTitle": "단위법", "settingsUnitSystemTitle": "단위법",
"settingsScreenSaverPageTitle": "화면보호기",
"settingsWidgetPageTitle": "사진 액자",
"settingsWidgetShowOutline": "윤곽",
"statsPageTitle": "통계", "statsPageTitle": "통계",
"statsWithGps": "{count, plural, other{{count}개 위치가 있음}}", "statsWithGps": "{count, plural, other{{count}개 위치가 있음}}",
"statsTopCountries": "국가 랭킹", "statsTopCountries": "국가 랭킹",

View file

@ -25,7 +25,8 @@
"showTooltip": "Mostrar", "showTooltip": "Mostrar",
"hideTooltip": "Ocultar", "hideTooltip": "Ocultar",
"actionRemove": "Remover", "actionRemove": "Remover",
"resetButtonTooltip": "Resetar", "resetTooltip": "Resetar",
"saveTooltip": "Salve",
"doubleBackExitMessage": "Toque em “voltar” novamente para sair.", "doubleBackExitMessage": "Toque em “voltar” novamente para sair.",
"doNotAskAgain": "Não pergunte novamente", "doNotAskAgain": "Não pergunte novamente",
@ -94,6 +95,7 @@
"filterFavouriteLabel": "Favorito", "filterFavouriteLabel": "Favorito",
"filterLocationEmptyLabel": "Não localizado", "filterLocationEmptyLabel": "Não localizado",
"filterTagEmptyLabel": "Sem etiqueta", "filterTagEmptyLabel": "Sem etiqueta",
"filterOnThisDayLabel": "Neste dia",
"filterRatingUnratedLabel": "Sem classificação", "filterRatingUnratedLabel": "Sem classificação",
"filterRatingRejectedLabel": "Rejeitado", "filterRatingRejectedLabel": "Rejeitado",
"filterTypeAnimatedLabel": "Animado", "filterTypeAnimatedLabel": "Animado",
@ -303,7 +305,6 @@
"aboutBug": "Relatório de erro", "aboutBug": "Relatório de erro",
"aboutBugSaveLogInstruction": "Salvar registros de aplicativos em um arquivo", "aboutBugSaveLogInstruction": "Salvar registros de aplicativos em um arquivo",
"aboutBugSaveLogButton": "Salve",
"aboutBugCopyInfoInstruction": "Copiar informações do sistema", "aboutBugCopyInfoInstruction": "Copiar informações do sistema",
"aboutBugCopyInfoButton": "Copiar", "aboutBugCopyInfoButton": "Copiar",
"aboutBugReportInstruction": "Relatório no GitHub com os logs e informações do sistema", "aboutBugReportInstruction": "Relatório no GitHub com os logs e informações do sistema",
@ -417,6 +418,7 @@
"searchCollectionFieldHint": "Pesquisar coleção", "searchCollectionFieldHint": "Pesquisar coleção",
"searchSectionRecent": "Recente", "searchSectionRecent": "Recente",
"searchSectionDate": "Data",
"searchSectionAlbums": "Álbuns", "searchSectionAlbums": "Álbuns",
"searchSectionCountries": "Países", "searchSectionCountries": "Países",
"searchSectionPlaces": "Locais", "searchSectionPlaces": "Locais",
@ -502,6 +504,7 @@
"settingsViewerSlideshowTitle": "Apresentação de slides", "settingsViewerSlideshowTitle": "Apresentação de slides",
"settingsSlideshowRepeat": "Repetir", "settingsSlideshowRepeat": "Repetir",
"settingsSlideshowShuffle": "Embaralhar", "settingsSlideshowShuffle": "Embaralhar",
"settingsSlideshowFillScreen": "Preencher tela",
"settingsSlideshowTransitionTile": "Transição", "settingsSlideshowTransitionTile": "Transição",
"settingsSlideshowTransitionTitle": "Transição", "settingsSlideshowTransitionTitle": "Transição",
"settingsSlideshowIntervalTile": "Intervalo", "settingsSlideshowIntervalTile": "Intervalo",
@ -584,6 +587,11 @@
"settingsUnitSystemTile": "Unidades", "settingsUnitSystemTile": "Unidades",
"settingsUnitSystemTitle": "Unidades", "settingsUnitSystemTitle": "Unidades",
"settingsScreenSaverPageTitle": "Protetor de tela",
"settingsWidgetPageTitle": "Porta-retratos",
"settingsWidgetShowOutline": "Contorno",
"statsPageTitle": "Estatísticas", "statsPageTitle": "Estatísticas",
"statsWithGps": "{count, plural, =1{1 item com localização} other{{count} itens com localização}}", "statsWithGps": "{count, plural, =1{1 item com localização} other{{count} itens com localização}}",
"statsTopCountries": "Principais Países", "statsTopCountries": "Principais Países",

View file

@ -25,7 +25,8 @@
"showTooltip": "Показать", "showTooltip": "Показать",
"hideTooltip": "Скрыть", "hideTooltip": "Скрыть",
"actionRemove": "Удалить", "actionRemove": "Удалить",
"resetButtonTooltip": "Сбросить", "resetTooltip": "Сбросить",
"saveTooltip": "Сохранить",
"doubleBackExitMessage": "Нажмите «Назад» еще раз, чтобы выйти.", "doubleBackExitMessage": "Нажмите «Назад» еще раз, чтобы выйти.",
"doNotAskAgain": "Больше не спрашивать", "doNotAskAgain": "Больше не спрашивать",
@ -81,6 +82,9 @@
"videoActionSetSpeed": "Скорость вопспроизведения", "videoActionSetSpeed": "Скорость вопспроизведения",
"videoActionSettings": "Настройки", "videoActionSettings": "Настройки",
"slideshowActionResume": "Продолжить",
"slideshowActionShowInCollection": "Показать в Коллекции",
"entryInfoActionEditDate": "Изменить дату и время", "entryInfoActionEditDate": "Изменить дату и время",
"entryInfoActionEditLocation": "Изменить местоположение", "entryInfoActionEditLocation": "Изменить местоположение",
"entryInfoActionEditRating": "Изменить рейтинг", "entryInfoActionEditRating": "Изменить рейтинг",
@ -145,10 +149,23 @@
"displayRefreshRatePreferHighest": "Наивысшая частота", "displayRefreshRatePreferHighest": "Наивысшая частота",
"displayRefreshRatePreferLowest": "Наименьшая частота", "displayRefreshRatePreferLowest": "Наименьшая частота",
"slideshowVideoPlaybackSkip": "Пропустить",
"slideshowVideoPlaybackMuted": "Играть без звука",
"slideshowVideoPlaybackWithSound": "Играть со звуком",
"themeBrightnessLight": "Светлая", "themeBrightnessLight": "Светлая",
"themeBrightnessDark": "Тёмная", "themeBrightnessDark": "Тёмная",
"themeBrightnessBlack": "Чёрная", "themeBrightnessBlack": "Чёрная",
"viewerTransitionSlide": "Скольжение",
"viewerTransitionParallax": "Параллакс",
"viewerTransitionFade": "Затухание",
"viewerTransitionZoomIn": "Приближение",
"wallpaperTargetHome": "Домашний экран",
"wallpaperTargetLock": "Экран блокировки",
"wallpaperTargetHomeLock": "Домашний экран и экран блокировки",
"albumTierNew": "Новые", "albumTierNew": "Новые",
"albumTierPinned": "Закрепленные", "albumTierPinned": "Закрепленные",
"albumTierSpecial": "Стандартные", "albumTierSpecial": "Стандартные",
@ -263,6 +280,7 @@
"menuActionSelectAll": "Выбрать все", "menuActionSelectAll": "Выбрать все",
"menuActionSelectNone": "Снять выделение", "menuActionSelectNone": "Снять выделение",
"menuActionMap": "Карта", "menuActionMap": "Карта",
"menuActionSlideshow": "Слайд-шоу",
"menuActionStats": "Статистика", "menuActionStats": "Статистика",
"viewDialogTabSort": "Сортировка", "viewDialogTabSort": "Сортировка",
@ -286,7 +304,6 @@
"aboutBug": "Отчет об ошибке", "aboutBug": "Отчет об ошибке",
"aboutBugSaveLogInstruction": "Сохраните логи приложения в файл", "aboutBugSaveLogInstruction": "Сохраните логи приложения в файл",
"aboutBugSaveLogButton": "Сохранить",
"aboutBugCopyInfoInstruction": "Скопируйте системную информацию", "aboutBugCopyInfoInstruction": "Скопируйте системную информацию",
"aboutBugCopyInfoButton": "Скопировать", "aboutBugCopyInfoButton": "Скопировать",
"aboutBugReportInstruction": "Отправьте отчёт об ошибке на GitHub вместе с логами и системной информацией", "aboutBugReportInstruction": "Отправьте отчёт об ошибке на GitHub вместе с логами и системной информацией",
@ -350,6 +367,7 @@
"collectionEmptyFavourites": "Нет избранных", "collectionEmptyFavourites": "Нет избранных",
"collectionEmptyVideos": "Нет видео", "collectionEmptyVideos": "Нет видео",
"collectionEmptyImages": "Нет изображений", "collectionEmptyImages": "Нет изображений",
"collectionEmptyGrantAccessButtonLabel": "Предоставить доступ",
"collectionSelectSectionTooltip": "Выбрать раздел", "collectionSelectSectionTooltip": "Выбрать раздел",
"collectionDeselectSectionTooltip": "Снять выбор с раздела", "collectionDeselectSectionTooltip": "Снять выбор с раздела",
@ -399,6 +417,7 @@
"searchCollectionFieldHint": "Поиск по коллекции", "searchCollectionFieldHint": "Поиск по коллекции",
"searchSectionRecent": "Недавние", "searchSectionRecent": "Недавние",
"searchSectionDate": "Дата",
"searchSectionAlbums": "Альбомы", "searchSectionAlbums": "Альбомы",
"searchSectionCountries": "Страны", "searchSectionCountries": "Страны",
"searchSectionPlaces": "Локации", "searchSectionPlaces": "Локации",
@ -480,6 +499,17 @@
"settingsViewerShowOverlayThumbnails": "Показать эскизы", "settingsViewerShowOverlayThumbnails": "Показать эскизы",
"settingsViewerEnableOverlayBlurEffect": "Наложение эффекта размытия", "settingsViewerEnableOverlayBlurEffect": "Наложение эффекта размытия",
"settingsViewerSlideshowTile": "Слайд-шоу",
"settingsViewerSlideshowTitle": "Слайд-шоу",
"settingsSlideshowRepeat": "Повтор",
"settingsSlideshowShuffle": "Вперемешку",
"settingsSlideshowTransitionTile": "Эффект перехода",
"settingsSlideshowTransitionTitle": "Эффект Перехода",
"settingsSlideshowIntervalTile": "Интервал",
"settingsSlideshowIntervalTitle": "Интервал",
"settingsSlideshowVideoPlaybackTile": "Проигрывание видео",
"settingsSlideshowVideoPlaybackTitle": "Проигрывание Видео",
"settingsVideoPageTitle": "Настройки видео", "settingsVideoPageTitle": "Настройки видео",
"settingsSectionVideo": "Видео", "settingsSectionVideo": "Видео",
"settingsVideoShowVideos": "Показать видео", "settingsVideoShowVideos": "Показать видео",
@ -544,6 +574,7 @@
"settingsSectionDisplay": "Отображение", "settingsSectionDisplay": "Отображение",
"settingsThemeBrightness": "Тема", "settingsThemeBrightness": "Тема",
"settingsThemeColorHighlights": "Цветовые акценты", "settingsThemeColorHighlights": "Цветовые акценты",
"settingsThemeEnableDynamicColor": "Динамический цвет",
"settingsDisplayRefreshRateModeTile": "Частота обновления экрана", "settingsDisplayRefreshRateModeTile": "Частота обновления экрана",
"settingsDisplayRefreshRateModeTitle": "Частота обновления", "settingsDisplayRefreshRateModeTitle": "Частота обновления",
@ -561,6 +592,7 @@
"statsTopTags": "Топ тегов", "statsTopTags": "Топ тегов",
"viewerOpenPanoramaButtonLabel": "ОТКРЫТЬ ПАНОРАМУ", "viewerOpenPanoramaButtonLabel": "ОТКРЫТЬ ПАНОРАМУ",
"viewerSetWallpaperButtonLabel": "УСТАНОВИТЬ КАК ОБОИ",
"viewerErrorUnknown": "Упс!", "viewerErrorUnknown": "Упс!",
"viewerErrorDoesNotExist": "Файл больше не существует.", "viewerErrorDoesNotExist": "Файл больше не существует.",

View file

@ -25,7 +25,8 @@
"showTooltip": "Göster", "showTooltip": "Göster",
"hideTooltip": "Gizle", "hideTooltip": "Gizle",
"actionRemove": "Kaldır", "actionRemove": "Kaldır",
"resetButtonTooltip": "Sıfırla", "resetTooltip": "Sıfırla",
"saveTooltip": "Kaydet",
"doubleBackExitMessage": "Çıkmak için tekrar “geri”, düğmesine dokunun.", "doubleBackExitMessage": "Çıkmak için tekrar “geri”, düğmesine dokunun.",
"doNotAskAgain": "Bir daha sorma", "doNotAskAgain": "Bir daha sorma",
@ -295,7 +296,6 @@
"aboutBug": "Hata Bildirimi", "aboutBug": "Hata Bildirimi",
"aboutBugSaveLogInstruction": "Uygulama günlüklerini bir dosyaya kaydet", "aboutBugSaveLogInstruction": "Uygulama günlüklerini bir dosyaya kaydet",
"aboutBugSaveLogButton": "Kaydet",
"aboutBugCopyInfoInstruction": "Sistem bilgilerini kopyala", "aboutBugCopyInfoInstruction": "Sistem bilgilerini kopyala",
"aboutBugCopyInfoButton": "Kopyala", "aboutBugCopyInfoButton": "Kopyala",
"aboutBugReportInstruction": "GitHub'da günlükleri ve sistem bilgilerini içeren bir rapor oluştur", "aboutBugReportInstruction": "GitHub'da günlükleri ve sistem bilgilerini içeren bir rapor oluştur",
@ -418,6 +418,7 @@
"searchCollectionFieldHint": "Koleksiyonu ara", "searchCollectionFieldHint": "Koleksiyonu ara",
"searchSectionRecent": "Yakın zamanda", "searchSectionRecent": "Yakın zamanda",
"searchSectionDate": "Tarih",
"searchSectionAlbums": "Albümler", "searchSectionAlbums": "Albümler",
"searchSectionCountries": "Ülkeler", "searchSectionCountries": "Ülkeler",
"searchSectionPlaces": "Yerler", "searchSectionPlaces": "Yerler",

View file

@ -25,7 +25,8 @@
"showTooltip": "显示", "showTooltip": "显示",
"hideTooltip": "隐藏", "hideTooltip": "隐藏",
"actionRemove": "移除", "actionRemove": "移除",
"resetButtonTooltip": "重置", "resetTooltip": "重置",
"saveTooltip": "保存",
"doubleBackExitMessage": "再按一次退出", "doubleBackExitMessage": "再按一次退出",
"doNotAskAgain": "不再询问", "doNotAskAgain": "不再询问",
@ -94,6 +95,7 @@
"filterFavouriteLabel": "收藏夹", "filterFavouriteLabel": "收藏夹",
"filterLocationEmptyLabel": "未定位", "filterLocationEmptyLabel": "未定位",
"filterTagEmptyLabel": "无标签", "filterTagEmptyLabel": "无标签",
"filterOnThisDayLabel": "选择日期",
"filterRatingUnratedLabel": "未评分", "filterRatingUnratedLabel": "未评分",
"filterRatingRejectedLabel": "拒绝", "filterRatingRejectedLabel": "拒绝",
"filterTypeAnimatedLabel": "动画", "filterTypeAnimatedLabel": "动画",
@ -303,7 +305,6 @@
"aboutBug": "报告错误", "aboutBug": "报告错误",
"aboutBugSaveLogInstruction": "将应用日志保存到文件", "aboutBugSaveLogInstruction": "将应用日志保存到文件",
"aboutBugSaveLogButton": "保存",
"aboutBugCopyInfoInstruction": "复制系统信息", "aboutBugCopyInfoInstruction": "复制系统信息",
"aboutBugCopyInfoButton": "复制", "aboutBugCopyInfoButton": "复制",
"aboutBugReportInstruction": "在 GitHub 上报告日志和系统信息", "aboutBugReportInstruction": "在 GitHub 上报告日志和系统信息",
@ -417,6 +418,7 @@
"searchCollectionFieldHint": "搜索媒体集", "searchCollectionFieldHint": "搜索媒体集",
"searchSectionRecent": "最近", "searchSectionRecent": "最近",
"searchSectionDate": "日期",
"searchSectionAlbums": "相册", "searchSectionAlbums": "相册",
"searchSectionCountries": "国家", "searchSectionCountries": "国家",
"searchSectionPlaces": "地点", "searchSectionPlaces": "地点",
@ -502,6 +504,7 @@
"settingsViewerSlideshowTitle": "幻灯片", "settingsViewerSlideshowTitle": "幻灯片",
"settingsSlideshowRepeat": "重复", "settingsSlideshowRepeat": "重复",
"settingsSlideshowShuffle": "随机播放", "settingsSlideshowShuffle": "随机播放",
"settingsSlideshowFillScreen": "填充屏幕",
"settingsSlideshowTransitionTile": "过渡动画", "settingsSlideshowTransitionTile": "过渡动画",
"settingsSlideshowTransitionTitle": "过渡动画", "settingsSlideshowTransitionTitle": "过渡动画",
"settingsSlideshowIntervalTile": "时间间隔", "settingsSlideshowIntervalTile": "时间间隔",
@ -584,6 +587,11 @@
"settingsUnitSystemTile": "单位", "settingsUnitSystemTile": "单位",
"settingsUnitSystemTitle": "单位", "settingsUnitSystemTitle": "单位",
"settingsScreenSaverPageTitle": "屏保",
"settingsWidgetPageTitle": "相框",
"settingsWidgetShowOutline": "轮廓",
"statsPageTitle": "统计", "statsPageTitle": "统计",
"statsWithGps": "{count, plural, other{{count} 项带位置信息}}", "statsWithGps": "{count, plural, other{{count} 项带位置信息}}",
"statsTopCountries": "热门国家", "statsTopCountries": "热门国家",

View file

@ -1,6 +1,10 @@
import 'package:aves/app_flavor.dart'; import 'package:aves/app_flavor.dart';
import 'package:aves/main_common.dart'; import 'package:aves/main_common.dart';
import 'package:aves/widget_common.dart';
void main() { const _flavor = AppFlavor.huawei;
mainCommon(AppFlavor.huawei);
} void main() => mainCommon(_flavor);
@pragma('vm:entry-point')
void widgetMain() => widgetMainCommon(_flavor);

View file

@ -1,6 +1,10 @@
import 'package:aves/app_flavor.dart'; import 'package:aves/app_flavor.dart';
import 'package:aves/main_common.dart'; import 'package:aves/main_common.dart';
import 'package:aves/widget_common.dart';
void main() { const _flavor = AppFlavor.izzy;
mainCommon(AppFlavor.izzy);
} void main() => mainCommon(_flavor);
@pragma('vm:entry-point')
void widgetMain() => widgetMainCommon(_flavor);

View file

@ -1,6 +1,10 @@
import 'package:aves/app_flavor.dart'; import 'package:aves/app_flavor.dart';
import 'package:aves/main_common.dart'; import 'package:aves/main_common.dart';
import 'package:aves/widget_common.dart';
void main() { const _flavor = AppFlavor.play;
mainCommon(AppFlavor.play);
} void main() => mainCommon(_flavor);
@pragma('vm:entry-point')
void widgetMain() => widgetMainCommon(_flavor);

View file

@ -249,7 +249,9 @@ class AvesEntry {
bool get is360 => _catalogMetadata?.is360 ?? false; bool get is360 => _catalogMetadata?.is360 ?? false;
bool get canEdit => path != null && !trashed; bool get isMediaStoreContent => uri.startsWith('content://media/');
bool get canEdit => path != null && !trashed && isMediaStoreContent;
bool get canEditDate => canEdit && (canEditExif || canEditXmp); bool get canEditDate => canEdit && (canEditExif || canEditXmp);
@ -689,7 +691,7 @@ class AvesEntry {
await metadataDb.removeIds({id}, dataTypes: dataTypes); await metadataDb.removeIds({id}, dataTypes: dataTypes);
} }
final updatedEntry = await mediaFileService.getEntry(uri, mimeType); final updatedEntry = await mediaFetchService.getEntry(uri, mimeType);
if (updatedEntry != null) { if (updatedEntry != null) {
await applyNewFields(updatedEntry.toMap(), persist: persist); await applyNewFields(updatedEntry.toMap(), persist: persist);
} }
@ -699,7 +701,7 @@ class AvesEntry {
Future<bool> delete() { Future<bool> delete() {
final completer = Completer<bool>(); final completer = Completer<bool>();
mediaFileService.delete(entries: {this}).listen( mediaEditService.delete(entries: {this}).listen(
(event) => completer.complete(event.success && !event.skipped), (event) => completer.complete(event.success && !event.skipped),
onError: completer.completeError, onError: completer.completeError,
onDone: () { onDone: () {

View file

@ -58,9 +58,13 @@ class EntryDir {
var resolved = vrl.volumePath; var resolved = vrl.volumePath;
final parts = pContext.split(vrl.relativeDir); final parts = pContext.split(vrl.relativeDir);
for (final part in parts) { for (final part in parts) {
FileSystemEntity? found;
final dir = Directory(resolved);
if (dir.existsSync()) {
final partLower = part.toLowerCase(); final partLower = part.toLowerCase();
final childrenDirs = Directory(resolved).listSync().where((v) => v.absolute is Directory).toSet(); final childrenDirs = dir.listSync().where((v) => v.absolute is Directory).toSet();
final found = childrenDirs.firstWhereOrNull((v) => pContext.basename(v.path).toLowerCase() == partLower); found = childrenDirs.firstWhereOrNull((v) => pContext.basename(v.path).toLowerCase() == partLower);
}
resolved = found?.path ?? '$resolved${pContext.separator}$part'; resolved = found?.path ?? '$resolved${pContext.separator}$part';
} }
return resolved; return resolved;

View file

@ -20,11 +20,12 @@ class AlbumFilter extends CoveredCollectionFilter {
const AlbumFilter(this.album, this.displayName); const AlbumFilter(this.album, this.displayName);
AlbumFilter.fromMap(Map<String, dynamic> json) factory AlbumFilter.fromMap(Map<String, dynamic> json) {
: this( return AlbumFilter(
json['album'], json['album'],
json['uniqueName'], json['uniqueName'],
); );
}
@override @override
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {

View file

@ -23,11 +23,12 @@ class CoordinateFilter extends CollectionFilter {
const CoordinateFilter(this.sw, this.ne, {this.minuteSecondPadding = false}); const CoordinateFilter(this.sw, this.ne, {this.minuteSecondPadding = false});
CoordinateFilter.fromMap(Map<String, dynamic> json) factory CoordinateFilter.fromMap(Map<String, dynamic> json) {
: this( return CoordinateFilter(
LatLng.fromJson(json['sw']), LatLng.fromJson(json['sw']),
LatLng.fromJson(json['ne']), LatLng.fromJson(json['ne']),
); );
}
@override @override
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {

137
lib/model/filters/date.dart Normal file
View file

@ -0,0 +1,137 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/format.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/time_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
class DateFilter extends CollectionFilter {
static const type = 'date';
final DateLevel level;
late final DateTime? date;
late final DateTime _effectiveDate;
late final EntryFilter _test;
static final onThisDay = DateFilter(DateLevel.md, null);
@override
List<Object?> get props => [level, date];
DateFilter(this.level, this.date) {
_effectiveDate = date ?? DateTime.now();
switch (level) {
case DateLevel.y:
_test = (entry) => entry.bestDate?.isAtSameYearAs(_effectiveDate) ?? false;
break;
case DateLevel.ym:
_test = (entry) => entry.bestDate?.isAtSameMonthAs(_effectiveDate) ?? false;
break;
case DateLevel.ymd:
_test = (entry) => entry.bestDate?.isAtSameDayAs(_effectiveDate) ?? false;
break;
case DateLevel.md:
final month = _effectiveDate.month;
final day = _effectiveDate.day;
_test = (entry) {
final bestDate = entry.bestDate;
return bestDate != null && bestDate.month == month && bestDate.day == day;
};
break;
case DateLevel.m:
final month = _effectiveDate.month;
_test = (entry) => entry.bestDate?.month == month;
break;
case DateLevel.d:
final day = _effectiveDate.day;
_test = (entry) => entry.bestDate?.day == day;
break;
}
}
factory DateFilter.fromMap(Map<String, dynamic> json) {
final dateString = json['date'] as String?;
return DateFilter(
DateLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? DateLevel.ymd,
dateString != null ? DateTime.tryParse(dateString) : null,
);
}
@override
Map<String, dynamic> toMap() => {
'type': type,
'level': level.toString(),
'date': date?.toIso8601String(),
};
@override
EntryFilter get test => _test;
@override
bool isCompatible(CollectionFilter other) {
if (other is DateFilter) {
return isCompatibleLevel(level, other.level);
} else {
return true;
}
}
static bool isCompatibleLevel(DateLevel a, DateLevel b) {
switch (a) {
case DateLevel.y:
return {DateLevel.md, DateLevel.m, DateLevel.d}.contains(b);
case DateLevel.ym:
return DateLevel.d == b;
case DateLevel.ymd:
return false;
case DateLevel.md:
return DateLevel.y == b;
case DateLevel.m:
return {DateLevel.y, DateLevel.d}.contains(b);
case DateLevel.d:
return {DateLevel.y, DateLevel.ym, DateLevel.m}.contains(b);
}
}
@override
String get universalLabel => _effectiveDate.toIso8601String();
@override
String getLabel(BuildContext context) {
final l10n = context.l10n;
final locale = l10n.localeName;
switch (level) {
case DateLevel.y:
return DateFormat.y(locale).format(_effectiveDate);
case DateLevel.ym:
return DateFormat.yMMM(locale).format(_effectiveDate);
case DateLevel.ymd:
return formatDay(_effectiveDate, locale);
case DateLevel.md:
if (date != null) {
return DateFormat.MMMd(locale).format(_effectiveDate);
} else {
return l10n.filterOnThisDayLabel;
}
case DateLevel.m:
return DateFormat.MMMM(locale).format(_effectiveDate);
case DateLevel.d:
return DateFormat.d(locale).format(_effectiveDate);
}
}
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
return Icon(AIcons.date, size: size);
}
@override
String get category => type;
@override
String get key => '$type-$level-$date';
}
enum DateLevel { y, ym, ymd, md, m, d }

View file

@ -4,6 +4,7 @@ import 'package:aves/model/covers.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/coordinate.dart'; import 'package:aves/model/filters/coordinate.dart';
import 'package:aves/model/filters/date.dart';
import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/location.dart';
import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/mime.dart';
@ -28,6 +29,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
MimeFilter.type, MimeFilter.type,
AlbumFilter.type, AlbumFilter.type,
TypeFilter.type, TypeFilter.type,
DateFilter.type,
LocationFilter.type, LocationFilter.type,
CoordinateFilter.type, CoordinateFilter.type,
FavouriteFilter.type, FavouriteFilter.type,
@ -52,6 +54,8 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
return AlbumFilter.fromMap(jsonMap); return AlbumFilter.fromMap(jsonMap);
case CoordinateFilter.type: case CoordinateFilter.type:
return CoordinateFilter.fromMap(jsonMap); return CoordinateFilter.fromMap(jsonMap);
case DateFilter.type:
return DateFilter.fromMap(jsonMap);
case FavouriteFilter.type: case FavouriteFilter.type:
return FavouriteFilter.instance; return FavouriteFilter.instance;
case LocationFilter.type: case LocationFilter.type:
@ -85,7 +89,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
EntryFilter get test; EntryFilter get test;
bool get isUnique => true; bool isCompatible(CollectionFilter other) => category != other.category;
String get universalLabel; String get universalLabel;

View file

@ -31,11 +31,12 @@ class LocationFilter extends CoveredCollectionFilter {
} }
} }
LocationFilter.fromMap(Map<String, dynamic> json) factory LocationFilter.fromMap(Map<String, dynamic> json) {
: this( return LocationFilter(
LocationLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? LocationLevel.place, LocationLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? LocationLevel.place,
json['location'], json['location'],
); );
}
@override @override
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {

View file

@ -43,10 +43,11 @@ class MimeFilter extends CollectionFilter {
_icon = icon ?? AIcons.vector; _icon = icon ?? AIcons.vector;
} }
MimeFilter.fromMap(Map<String, dynamic> json) factory MimeFilter.fromMap(Map<String, dynamic> json) {
: this( return MimeFilter(
json['mime'], json['mime'],
); );
}
@override @override
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {

View file

@ -15,10 +15,11 @@ class PathFilter extends CollectionFilter {
PathFilter(this.path) : _rootAlbum = path.substring(0, path.length - 1); PathFilter(this.path) : _rootAlbum = path.substring(0, path.length - 1);
PathFilter.fromMap(Map<String, dynamic> json) factory PathFilter.fromMap(Map<String, dynamic> json) {
: this( return PathFilter(
json['path'], json['path'],
); );
}
@override @override
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {

View file

@ -59,10 +59,11 @@ class QueryFilter extends CollectionFilter {
_test = not ? (entry) => !testTitle(entry) : testTitle; _test = not ? (entry) => !testTitle(entry) : testTitle;
} }
QueryFilter.fromMap(Map<String, dynamic> json) factory QueryFilter.fromMap(Map<String, dynamic> json) {
: this( return QueryFilter(
json['query'], json['query'],
); );
}
@override @override
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
@ -74,7 +75,7 @@ class QueryFilter extends CollectionFilter {
EntryFilter get test => _test; EntryFilter get test => _test;
@override @override
bool get isUnique => false; bool isCompatible(CollectionFilter other) => true;
@override @override
String get universalLabel => query; String get universalLabel => query;

View file

@ -13,10 +13,11 @@ class RatingFilter extends CollectionFilter {
const RatingFilter(this.rating); const RatingFilter(this.rating);
RatingFilter.fromMap(Map<String, dynamic> json) factory RatingFilter.fromMap(Map<String, dynamic> json) {
: this( return RatingFilter(
json['rating'] ?? 0, json['rating'] ?? 0,
); );
}
@override @override
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {

View file

@ -20,11 +20,12 @@ class TagFilter extends CoveredCollectionFilter {
} }
} }
TagFilter.fromMap(Map<String, dynamic> json) factory TagFilter.fromMap(Map<String, dynamic> json) {
: this( return TagFilter(
json['tag'], json['tag'],
not: json['not'] ?? false, not: json['not'] ?? false,
); );
}
@override @override
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
@ -37,7 +38,7 @@ class TagFilter extends CoveredCollectionFilter {
EntryFilter get test => _test; EntryFilter get test => _test;
@override @override
bool get isUnique => false; bool isCompatible(CollectionFilter other) => true;
@override @override
String get universalLabel => tag; String get universalLabel => tag;

View file

@ -59,10 +59,11 @@ class TypeFilter extends CollectionFilter {
} }
} }
TypeFilter.fromMap(Map<String, dynamic> json) factory TypeFilter.fromMap(Map<String, dynamic> json) {
: this._private( return TypeFilter._private(
json['itemType'], json['itemType'],
); );
}
@override @override
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {

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;
@ -128,10 +128,15 @@ class SettingsDefaults {
// slideshow // slideshow
static const slideshowRepeat = false; static const slideshowRepeat = false;
static const slideshowShuffle = false; static const slideshowShuffle = false;
static const slideshowFillScreen = false;
static const slideshowTransition = ViewerTransition.fade; static const slideshowTransition = ViewerTransition.fade;
static const slideshowVideoPlayback = SlideshowVideoPlayback.playMuted; static const slideshowVideoPlayback = SlideshowVideoPlayback.playMuted;
static const slideshowInterval = SlideshowInterval.s5; static const slideshowInterval = SlideshowInterval.s5;
// widget
static const widgetOutline = false;
static const widgetShape = WidgetShape.rrect;
// platform settings // platform settings
static const isRotationLocked = false; static const isRotationLocked = false;
static const areAnimationsRemoved = false; static const areAnimationsRemoved = false;

View file

@ -1,4 +1,6 @@
import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart';
@ -16,17 +18,22 @@ extension ExtraDisplayRefreshRateMode on DisplayRefreshRateMode {
} }
} }
void apply() { Future<void> apply() async {
if (!await windowService.isActivity()) return;
final androidInfo = await DeviceInfoPlugin().androidInfo;
if ((androidInfo.version.sdkInt ?? 0) < 23) return;
debugPrint('Apply display refresh rate: $name'); debugPrint('Apply display refresh rate: $name');
switch (this) { switch (this) {
case DisplayRefreshRateMode.auto: case DisplayRefreshRateMode.auto:
FlutterDisplayMode.setPreferredMode(DisplayMode.auto); await FlutterDisplayMode.setPreferredMode(DisplayMode.auto);
break; break;
case DisplayRefreshRateMode.highest: case DisplayRefreshRateMode.highest:
FlutterDisplayMode.setHighRefreshRate(); await FlutterDisplayMode.setHighRefreshRate();
break; break;
case DisplayRefreshRateMode.lowest: case DisplayRefreshRateMode.lowest:
FlutterDisplayMode.setLowRefreshRate(); await FlutterDisplayMode.setLowRefreshRate();
break; break;
} }
} }

View file

@ -29,3 +29,5 @@ enum VideoControls { play, playSeek, playOutside, none }
enum VideoLoopMode { never, shortOnly, always } enum VideoLoopMode { never, shortOnly, always }
enum ViewerTransition { slide, parallax, fade, zoomIn } enum ViewerTransition { slide, parallax, fade, zoomIn }
enum WidgetShape { rrect, circle, heart }

View file

@ -0,0 +1,44 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:flutter/material.dart';
extension ExtraWidgetShape on WidgetShape {
Path path(Size widgetSize, double devicePixelRatio) {
final rect = Rect.fromLTWH(0, 0, widgetSize.width, widgetSize.height);
switch (this) {
case WidgetShape.rrect:
return Path()..addRRect(BorderRadius.circular(24 * devicePixelRatio).toRRect(rect));
case WidgetShape.circle:
return Path()
..addOval(Rect.fromCircle(
center: rect.center,
radius: rect.shortestSide / 2,
));
case WidgetShape.heart:
final center = rect.center;
final dim = rect.shortestSide;
const p0dy = -.4;
const p1dx = .5;
const p1dy = -.4;
const p2dx = .8;
const p2dy = .5;
const p3dy = .5 - p0dy;
return Path()
..moveTo(center.dx, center.dy)
..relativeMoveTo(0, dim * p0dy)
..relativeCubicTo(dim * -p1dx, dim * p1dy, dim * -p2dx, dim * p2dy, 0, dim * p3dy)
..moveTo(center.dx, center.dy)
..relativeMoveTo(0, dim * p0dy)
..relativeCubicTo(dim * p1dx, dim * p1dy, dim * p2dx, dim * p2dy, 0, dim * p3dy);
}
}
Size size(Size widgetSize) {
switch (this) {
case WidgetShape.rrect:
return widgetSize;
case WidgetShape.circle:
case WidgetShape.heart:
return Size.square(widgetSize.shortestSide);
}
}
}

View file

@ -27,7 +27,7 @@ class Settings extends ChangeNotifier {
Settings._private(); Settings._private();
static const Set<String> internalKeys = { static const Set<String> _internalKeys = {
hasAcceptedTermsKey, hasAcceptedTermsKey,
catalogTimeZoneKey, catalogTimeZoneKey,
videoShowRawTimedTextKey, videoShowRawTimedTextKey,
@ -36,6 +36,7 @@ class Settings extends ChangeNotifier {
platformTransitionAnimationScaleKey, platformTransitionAnimationScaleKey,
topEntryIdsKey, topEntryIdsKey,
}; };
static const _widgetKeyPrefix = 'widget_';
// app // app
static const hasAcceptedTermsKey = 'has_accepted_terms'; static const hasAcceptedTermsKey = 'has_accepted_terms';
@ -60,7 +61,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';
@ -138,13 +139,27 @@ class Settings extends ChangeNotifier {
// file picker // file picker
static const filePickerShowHiddenFilesKey = 'file_picker_show_hidden_files'; static const filePickerShowHiddenFilesKey = 'file_picker_show_hidden_files';
// screen saver
static const screenSaverFillScreenKey = 'screen_saver_fill_screen';
static const screenSaverTransitionKey = 'screen_saver_transition';
static const screenSaverVideoPlaybackKey = 'screen_saver_video_playback';
static const screenSaverIntervalKey = 'screen_saver_interval';
static const screenSaverCollectionFiltersKey = 'screen_saver_collection_filters';
// slideshow // slideshow
static const slideshowRepeatKey = 'slideshow_loop'; static const slideshowRepeatKey = 'slideshow_loop';
static const slideshowShuffleKey = 'slideshow_shuffle'; static const slideshowShuffleKey = 'slideshow_shuffle';
static const slideshowFillScreenKey = 'slideshow_fill_screen';
static const slideshowTransitionKey = 'slideshow_transition'; static const slideshowTransitionKey = 'slideshow_transition';
static const slideshowVideoPlaybackKey = 'slideshow_video_playback'; static const slideshowVideoPlaybackKey = 'slideshow_video_playback';
static const slideshowIntervalKey = 'slideshow_interval'; static const slideshowIntervalKey = 'slideshow_interval';
// widget
static const widgetOutlinePrefixKey = '${_widgetKeyPrefix}outline_';
static const widgetShapePrefixKey = '${_widgetKeyPrefix}shape_';
static const widgetCollectionFiltersPrefixKey = '${_widgetKeyPrefix}collection_filters_';
static const widgetUriPrefixKey = '${_widgetKeyPrefix}uri_';
// platform settings // platform settings
// cf Android `Settings.System.ACCELEROMETER_ROTATION` // cf Android `Settings.System.ACCELEROMETER_ROTATION`
static const platformAccelerometerRotationKey = 'accelerometer_rotation'; static const platformAccelerometerRotationKey = 'accelerometer_rotation';
@ -161,14 +176,18 @@ class Settings extends ChangeNotifier {
} }
} }
Future<void> reload() => settingsStore.reload();
Future<void> reset({required bool includeInternalKeys}) async { Future<void> reset({required bool includeInternalKeys}) async {
if (includeInternalKeys) { if (includeInternalKeys) {
await settingsStore.clear(); await settingsStore.clear();
} else { } else {
await Future.forEach<String>(settingsStore.getKeys().whereNot(Settings.internalKeys.contains), settingsStore.remove); await Future.forEach<String>(settingsStore.getKeys().whereNot(isInternalKey), settingsStore.remove);
} }
} }
bool isInternalKey(String key) => _internalKeys.contains(key) || key.startsWith(_widgetKeyPrefix);
Future<void> setContextualDefaults() async { Future<void> setContextualDefaults() async {
// performance // performance
final performanceClass = await deviceService.getPerformanceClass(); final performanceClass = await deviceService.getPerformanceClass();
@ -315,9 +334,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);
@ -583,6 +602,28 @@ class Settings extends ChangeNotifier {
set filePickerShowHiddenFiles(bool newValue) => setAndNotify(filePickerShowHiddenFilesKey, newValue); set filePickerShowHiddenFiles(bool newValue) => setAndNotify(filePickerShowHiddenFilesKey, newValue);
// screen saver
bool get screenSaverFillScreen => getBoolOrDefault(screenSaverFillScreenKey, SettingsDefaults.slideshowFillScreen);
set screenSaverFillScreen(bool newValue) => setAndNotify(screenSaverFillScreenKey, newValue);
ViewerTransition get screenSaverTransition => getEnumOrDefault(screenSaverTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values);
set screenSaverTransition(ViewerTransition newValue) => setAndNotify(screenSaverTransitionKey, newValue.toString());
SlideshowVideoPlayback get screenSaverVideoPlayback => getEnumOrDefault(screenSaverVideoPlaybackKey, SettingsDefaults.slideshowVideoPlayback, SlideshowVideoPlayback.values);
set screenSaverVideoPlayback(SlideshowVideoPlayback newValue) => setAndNotify(screenSaverVideoPlaybackKey, newValue.toString());
SlideshowInterval get screenSaverInterval => getEnumOrDefault(screenSaverIntervalKey, SettingsDefaults.slideshowInterval, SlideshowInterval.values);
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);
@ -593,6 +634,10 @@ class Settings extends ChangeNotifier {
set slideshowShuffle(bool newValue) => setAndNotify(slideshowShuffleKey, newValue); set slideshowShuffle(bool newValue) => setAndNotify(slideshowShuffleKey, newValue);
bool get slideshowFillScreen => getBoolOrDefault(slideshowFillScreenKey, SettingsDefaults.slideshowFillScreen);
set slideshowFillScreen(bool newValue) => setAndNotify(slideshowFillScreenKey, newValue);
ViewerTransition get slideshowTransition => getEnumOrDefault(slideshowTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values); ViewerTransition get slideshowTransition => getEnumOrDefault(slideshowTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values);
set slideshowTransition(ViewerTransition newValue) => setAndNotify(slideshowTransitionKey, newValue.toString()); set slideshowTransition(ViewerTransition newValue) => setAndNotify(slideshowTransitionKey, newValue.toString());
@ -605,6 +650,27 @@ class Settings extends ChangeNotifier {
set slideshowInterval(SlideshowInterval newValue) => setAndNotify(slideshowIntervalKey, newValue.toString()); set slideshowInterval(SlideshowInterval newValue) => setAndNotify(slideshowIntervalKey, newValue.toString());
// widget
Color? getWidgetOutline(int widgetId) {
final value = getInt('$widgetOutlinePrefixKey$widgetId');
return value != null ? Color(value) : null;
}
void setWidgetOutline(int widgetId, Color? newValue) => setAndNotify('$widgetOutlinePrefixKey$widgetId', newValue?.value);
WidgetShape getWidgetShape(int widgetId) => getEnumOrDefault('$widgetShapePrefixKey$widgetId', SettingsDefaults.widgetShape, WidgetShape.values);
void setWidgetShape(int widgetId, WidgetShape newValue) => setAndNotify('$widgetShapePrefixKey$widgetId', newValue.toString());
Set<CollectionFilter> getWidgetCollectionFilters(int widgetId) => (getStringList('$widgetCollectionFiltersPrefixKey$widgetId') ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
void setWidgetCollectionFilters(int widgetId, Set<CollectionFilter> newValue) => setAndNotify('$widgetCollectionFiltersPrefixKey$widgetId', newValue.map((filter) => filter.toJson()).toList());
String? getWidgetUri(int widgetId) => getString('$widgetUriPrefixKey$widgetId');
void setWidgetUri(int widgetId, String? newValue) => setAndNotify('$widgetUriPrefixKey$widgetId', newValue);
// convenience methods // convenience methods
int? getInt(String key) => settingsStore.getInt(key); int? getInt(String key) => settingsStore.getInt(key);
@ -687,7 +753,7 @@ class Settings extends ChangeNotifier {
// import/export // import/export
Map<String, dynamic> export() => Map.fromEntries( Map<String, dynamic> export() => Map.fromEntries(
settingsStore.getKeys().whereNot(internalKeys.contains).map((k) => MapEntry(k, settingsStore.get(k))), settingsStore.getKeys().whereNot(isInternalKey).map((k) => MapEntry(k, settingsStore.get(k))),
); );
Future<void> import(dynamic jsonMap) async { Future<void> import(dynamic jsonMap) async {
@ -735,7 +801,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:
@ -763,8 +829,10 @@ class Settings extends ChangeNotifier {
case subtitleShowOutlineKey: case subtitleShowOutlineKey:
case saveSearchHistoryKey: case saveSearchHistoryKey:
case filePickerShowHiddenFilesKey: case filePickerShowHiddenFilesKey:
case screenSaverFillScreenKey:
case slideshowRepeatKey: case slideshowRepeatKey:
case slideshowShuffleKey: case slideshowShuffleKey:
case slideshowFillScreenKey:
if (newValue is bool) { if (newValue is bool) {
settingsStore.setBool(key, newValue); settingsStore.setBool(key, newValue);
} else { } else {
@ -792,6 +860,9 @@ class Settings extends ChangeNotifier {
case unitSystemKey: case unitSystemKey:
case accessibilityAnimationsKey: case accessibilityAnimationsKey:
case timeToTakeActionKey: case timeToTakeActionKey:
case screenSaverTransitionKey:
case screenSaverVideoPlaybackKey:
case screenSaverIntervalKey:
case slideshowTransitionKey: case slideshowTransitionKey:
case slideshowVideoPlaybackKey: case slideshowVideoPlaybackKey:
case slideshowIntervalKey: case slideshowIntervalKey:
@ -809,6 +880,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

@ -3,6 +3,8 @@ abstract class SettingsStore {
Future<void> init(); Future<void> init();
Future<void> reload();
Future<bool> clear(); Future<bool> clear();
Future<bool> remove(String key); Future<bool> remove(String key);

View file

@ -17,6 +17,9 @@ class SharedPrefSettingsStore implements SettingsStore {
} }
} }
@override
Future<void> reload() => _prefs!.reload();
@override @override
Future<bool> clear() => _prefs!.clear(); Future<bool> clear() => _prefs!.clear();

Some files were not shown because too many files have changed in this diff Show more