Merge branch 'develop'
This commit is contained in:
commit
968f6da395
238 changed files with 4807 additions and 1683 deletions
4
.github/workflows/check.yml
vendored
4
.github/workflows/check.yml
vendored
|
@ -17,8 +17,8 @@ jobs:
|
|||
# Available versions may lag behind https://github.com/flutter/flutter.git
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.0.2'
|
||||
channel: 'stable'
|
||||
flutter-version: '3.3.0-0.0.pre'
|
||||
channel: 'beta'
|
||||
|
||||
- name: Clone the repository.
|
||||
uses: actions/checkout@v2
|
||||
|
|
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
|
@ -19,8 +19,8 @@ jobs:
|
|||
# Available versions may lag behind https://github.com/flutter/flutter.git
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.0.2'
|
||||
channel: 'stable'
|
||||
flutter-version: '3.3.0-0.0.pre'
|
||||
channel: 'beta'
|
||||
|
||||
# Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1):
|
||||
# https://issuetracker.google.com/issues/144111441
|
||||
|
@ -56,15 +56,15 @@ jobs:
|
|||
rm release.keystore.asc
|
||||
mkdir outputs
|
||||
(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
|
||||
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
|
||||
(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
|
||||
(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
|
||||
rm $AVES_STORE_FILE
|
||||
env:
|
||||
|
|
19
CHANGELOG.md
19
CHANGELOG.md
|
@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## <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
|
||||
|
||||
### Added
|
||||
|
|
|
@ -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.
|
||||
|
||||
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
|
||||
|
||||
|
@ -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"
|
||||
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"
|
||||
alt='Donate using Liberapay'
|
||||
height="40">](https://liberapay.com/deckerst/donate)
|
||||
|
|
|
@ -5,18 +5,25 @@
|
|||
"DE":"connect-dre.dbankcloud.cn",
|
||||
"DE_back":"connect-dre.hispace.hicloud.com",
|
||||
"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_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":{
|
||||
"cp_id":"2640082000020010713",
|
||||
"product_id":"99536292102197525",
|
||||
"client_id":"874325707927340288",
|
||||
"client_secret":"DCAFAE5C0440ABDBD6DDB2B6EBD7D9B0870C10FCA64759CCD63020D168803AB5",
|
||||
"project_id":"99536292102197525",
|
||||
"app_id":"106014023",
|
||||
"api_key":"DAEDAEzScQA5ri36P2NEiVPSFrOJeYZ0DbEJZMGJrBadW+QudBr5BGHD3vO0tsL1VeBy0RPZefPic3hAWUijcBxCv0zRv0iBjQEptQ==",
|
||||
"package_name":"deckers.thibault.aves"
|
||||
},
|
||||
"oauth_client":{
|
||||
|
@ -30,17 +37,17 @@
|
|||
"configuration_version":"3.0",
|
||||
"appInfos":[
|
||||
{
|
||||
"package_name":"deckers.thibault.aves.profile",
|
||||
"package_name":"deckers.thibault.aves",
|
||||
"client":{
|
||||
"app_id":"106031461"
|
||||
"app_id":"106014023"
|
||||
},
|
||||
"app_info":{
|
||||
"package_name":"deckers.thibault.aves.profile",
|
||||
"app_id":"106031461"
|
||||
"package_name":"deckers.thibault.aves",
|
||||
"app_id":"106014023"
|
||||
},
|
||||
"oauth_client":{
|
||||
"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":{
|
||||
"app_id":"106014023"
|
||||
"app_id":"106031461"
|
||||
},
|
||||
"app_info":{
|
||||
"package_name":"deckers.thibault.aves",
|
||||
"app_id":"106014023"
|
||||
"package_name":"deckers.thibault.aves.profile",
|
||||
"app_id":"106031461"
|
||||
},
|
||||
"oauth_client":{
|
||||
"client_type":1,
|
||||
"client_id":"106014023"
|
||||
"client_id":"106031461"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -38,6 +38,7 @@ if (keystorePropertiesFile.exists()) {
|
|||
keystoreProperties['keyAlias'] = System.getenv('AVES_KEY_ALIAS')
|
||||
keystoreProperties['keyPassword'] = System.getenv('AVES_KEY_PASSWORD')
|
||||
keystoreProperties['googleApiKey'] = System.getenv('AVES_GOOGLE_API_KEY')
|
||||
keystoreProperties['huaweiApiKey'] = System.getenv('AVES_HUAWEI_API_KEY')
|
||||
}
|
||||
|
||||
android {
|
||||
|
@ -47,7 +48,6 @@ android {
|
|||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
|
||||
defaultConfig {
|
||||
applicationId appId
|
||||
// minSdkVersion constraints:
|
||||
|
@ -60,7 +60,8 @@ android {
|
|||
targetSdkVersion 33
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey']]
|
||||
manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey'],
|
||||
huaweiApiKey: keystoreProperties['huaweiApiKey']]
|
||||
multiDexEnabled true
|
||||
resValue 'string', 'search_provider', "${appId}.search_provider"
|
||||
}
|
||||
|
@ -140,7 +141,6 @@ android {
|
|||
lint {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
namespace 'deckers.thibault.aves'
|
||||
}
|
||||
|
||||
flutter {
|
||||
|
@ -169,7 +169,7 @@ dependencies {
|
|||
// huawei flavor only
|
||||
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'
|
||||
|
||||
compileOnly rootProject.findProject(':streams_channel')
|
||||
|
|
2
android/app/src/debug/res/xml/screen_saver.xml
Normal file
2
android/app/src/debug/res/xml/screen_saver.xml
Normal 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" />
|
|
@ -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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="deckers.thibault.aves"
|
||||
android:installLocation="auto">
|
||||
|
||||
<!--
|
||||
|
@ -134,6 +143,11 @@
|
|||
android:resource="@xml/searchable" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ScreenSaverSettingsActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/NormalTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".WallpaperActivity"
|
||||
android:exported="true"
|
||||
|
@ -153,11 +167,49 @@
|
|||
</intent-filter>
|
||||
</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
|
||||
android:name=".AnalysisService"
|
||||
android:description="@string/analysis_service_description"
|
||||
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 -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
@ -178,6 +230,9 @@
|
|||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="${googleApiKey}" />
|
||||
<meta-data
|
||||
android:name="deckers.thibault.aves.huawei.API_KEY"
|
||||
android:value="${huaweiApiKey}" />
|
||||
<meta-data
|
||||
android:name="firebase_crashlytics_collection_enabled"
|
||||
android:value="false" />
|
||||
|
|
|
@ -11,10 +11,7 @@ import androidx.core.app.NotificationCompat
|
|||
import androidx.core.app.NotificationManagerCompat
|
||||
import app.loup.streams_channel.StreamsChannel
|
||||
import deckers.thibault.aves.MainActivity.Companion.OPEN_FROM_ANALYSIS_SERVICE
|
||||
import deckers.thibault.aves.channel.calls.DeviceHandler
|
||||
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.calls.*
|
||||
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
|
||||
import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
|
||||
import deckers.thibault.aves.utils.FlutterUtils
|
||||
|
@ -25,7 +22,7 @@ import io.flutter.plugin.common.MethodChannel
|
|||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class AnalysisService : MethodChannel.MethodCallHandler, Service() {
|
||||
private var backgroundFlutterEngine: FlutterEngine? = null
|
||||
private var flutterEngine: FlutterEngine? = null
|
||||
private var backgroundChannel: MethodChannel? = null
|
||||
private var serviceLooper: Looper? = null
|
||||
private var serviceHandler: ServiceHandler? = null
|
||||
|
@ -37,18 +34,27 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
|
|||
|
||||
runBlocking {
|
||||
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
|
||||
|
||||
// dart -> platform -> dart
|
||||
// - need Context
|
||||
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
|
||||
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
|
||||
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
|
||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
||||
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, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) }
|
||||
|
||||
// channel for service management
|
||||
backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply {
|
||||
setMethodCallHandler(context)
|
||||
|
@ -67,7 +73,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
|
|||
|
||||
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)
|
||||
.setName(getText(R.string.analysis_channel_name))
|
||||
.setShowBadge(false)
|
||||
|
@ -76,7 +82,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
|
|||
startForeground(NOTIFICATION_ID, buildNotification())
|
||||
|
||||
val msgData = Bundle()
|
||||
intent.extras?.let {
|
||||
intent?.extras?.let {
|
||||
msgData.putAll(it)
|
||||
}
|
||||
serviceHandler?.obtainMessage()?.let { msg ->
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -2,10 +2,14 @@ package deckers.thibault.aves
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.SearchManager
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
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 androidx.annotation.RequiresApi
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
|
@ -13,6 +17,8 @@ import androidx.core.content.pm.ShortcutManagerCompat
|
|||
import androidx.core.graphics.drawable.IconCompat
|
||||
import app.loup.streams_channel.StreamsChannel
|
||||
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.utils.LogUtils
|
||||
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.ConcurrentHashMap
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
open class MainActivity : FlutterActivity() {
|
||||
private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler
|
||||
private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler
|
||||
private lateinit var intentStreamHandler: IntentStreamHandler
|
||||
private lateinit var analysisStreamHandler: AnalysisStreamHandler
|
||||
private lateinit var intentDataMap: MutableMap<String, Any?>
|
||||
internal lateinit var intentDataMap: MutableMap<String, Any?>
|
||||
private lateinit var analysisHandler: AnalysisHandler
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -51,7 +57,7 @@ class MainActivity : FlutterActivity() {
|
|||
// )
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val messenger = flutterEngine!!.dartExecutor.binaryMessenger
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
|
||||
// dart -> platform -> dart
|
||||
// - need Context
|
||||
|
@ -63,14 +69,17 @@ class MainActivity : FlutterActivity() {
|
|||
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
|
||||
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(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, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
|
||||
// - need Activity
|
||||
// - need ContextWrapper
|
||||
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, WindowHandler.CHANNEL).setMethodCallHandler(WindowHandler(this))
|
||||
// - need Activity
|
||||
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
|
||||
|
||||
// result streaming: dart -> platform ->->-> dart
|
||||
// - need Context
|
||||
|
@ -78,7 +87,7 @@ class MainActivity : FlutterActivity() {
|
|||
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) }
|
||||
// - need Activity
|
||||
StreamsChannel(messenger, ImageOpStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageOpStreamHandler(this, args) }
|
||||
StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL).setStreamHandlerFactory { args -> StorageAccessStreamHandler(this, args) }
|
||||
StreamsChannel(messenger, ActivityResultStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ActivityResultStreamHandler(this, args) }
|
||||
|
||||
// change monitoring: platform -> dart
|
||||
mediaStoreChangeStreamHandler = MediaStoreChangeStreamHandler(this).apply {
|
||||
|
@ -93,15 +102,16 @@ class MainActivity : FlutterActivity() {
|
|||
intentStreamHandler = IntentStreamHandler().apply {
|
||||
EventChannel(messenger, IntentStreamHandler.CHANNEL).setStreamHandler(this)
|
||||
}
|
||||
// detail fetch: dart -> platform
|
||||
// intent detail & result: dart -> platform
|
||||
intentDataMap = extractIntentData(intent)
|
||||
MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result ->
|
||||
MethodChannel(messenger, INTENT_CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getIntentData" -> {
|
||||
result.success(intentDataMap)
|
||||
intentDataMap.clear()
|
||||
}
|
||||
"pick" -> pick(call)
|
||||
"submitPickedItems" -> submitPickedItems(call)
|
||||
"submitPickedCollectionFilters" -> submitPickedCollectionFilters(call)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,27 +166,33 @@ class MainActivity : FlutterActivity() {
|
|||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(data, resultCode, requestCode)
|
||||
DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(requestCode, resultCode, data)
|
||||
DELETE_SINGLE_PERMISSION_REQUEST,
|
||||
MEDIA_WRITE_BULK_PERMISSION_REQUEST -> onScopedStoragePermissionResult(resultCode)
|
||||
CREATE_FILE_REQUEST,
|
||||
OPEN_FILE_REQUEST -> onStorageAccessResult(requestCode, data?.data)
|
||||
PICK_COLLECTION_FILTERS_REQUEST -> onCollectionFiltersPickResult(resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("WrongConstant", "ObsoleteSdkInt")
|
||||
private fun onDocumentTreeAccessResult(data: Intent?, resultCode: Int, requestCode: Int) {
|
||||
val treeUri = data?.data
|
||||
private fun onCollectionFiltersPickResult(resultCode: Int, intent: Intent?) {
|
||||
val filters = if (resultCode == RESULT_OK) extractFiltersFromIntent(intent) else null
|
||||
pendingCollectionFilterPickHandler?.let { it(filters) }
|
||||
}
|
||||
|
||||
private fun onDocumentTreeAccessResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
val treeUri = intent?.data
|
||||
if (resultCode != RESULT_OK || treeUri == null) {
|
||||
onStorageAccessResult(requestCode, null)
|
||||
return
|
||||
}
|
||||
|
||||
@SuppressLint("WrongConstant", "ObsoleteSdkInt")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
val canPersist = (data.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0
|
||||
val canPersist = (intent.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0
|
||||
if (canPersist) {
|
||||
// save access permissions across reboots
|
||||
val takeFlags = (data.flags
|
||||
val takeFlags = (intent.flags
|
||||
and (Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
|
||||
try {
|
||||
|
@ -197,18 +213,11 @@ class MainActivity : FlutterActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
||||
when (intent?.action) {
|
||||
open fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
||||
when (val action = intent?.action) {
|
||||
Intent.ACTION_MAIN -> {
|
||||
intent.getStringExtra(SHORTCUT_KEY_PAGE)?.let { page ->
|
||||
var filters = intent.getStringArrayExtra(SHORTCUT_KEY_FILTERS_ARRAY)?.toList()
|
||||
if (filters == null) {
|
||||
// fallback for shortcuts created on API < 26
|
||||
val filterString = intent.getStringExtra(SHORTCUT_KEY_FILTERS_STRING)
|
||||
if (filterString != null) {
|
||||
filters = filterString.split(EXTRA_STRING_ARRAY_SEPARATOR)
|
||||
}
|
||||
}
|
||||
intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page ->
|
||||
val filters = extractFiltersFromIntent(intent)
|
||||
return hashMapOf(
|
||||
INTENT_DATA_KEY_PAGE to page,
|
||||
INTENT_DATA_KEY_FILTERS to filters,
|
||||
|
@ -228,7 +237,7 @@ class MainActivity : FlutterActivity() {
|
|||
}
|
||||
Intent.ACTION_GET_CONTENT, Intent.ACTION_PICK -> {
|
||||
return hashMapOf(
|
||||
INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK,
|
||||
INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK_ITEMS,
|
||||
INTENT_DATA_KEY_MIME_TYPE to intent.type,
|
||||
INTENT_DATA_KEY_ALLOW_MULTIPLE to (intent.extras?.getBoolean(Intent.EXTRA_ALLOW_MULTIPLE) ?: false),
|
||||
)
|
||||
|
@ -244,6 +253,22 @@ class MainActivity : FlutterActivity() {
|
|||
INTENT_DATA_KEY_QUERY to intent.getStringExtra(SearchManager.QUERY),
|
||||
)
|
||||
}
|
||||
INTENT_ACTION_PICK_COLLECTION_FILTERS -> {
|
||||
val initialFilters = extractFiltersFromIntent(intent)
|
||||
return hashMapOf(
|
||||
INTENT_DATA_KEY_ACTION to 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 -> {
|
||||
// flutter run
|
||||
}
|
||||
|
@ -254,7 +279,22 @@ class MainActivity : FlutterActivity() {
|
|||
return HashMap()
|
||||
}
|
||||
|
||||
private fun pick(call: MethodCall) {
|
||||
private fun extractFiltersFromIntent(intent: Intent?): List<String>? {
|
||||
intent ?: return null
|
||||
|
||||
val filters = intent.getStringArrayExtra(EXTRA_KEY_FILTERS_ARRAY)?.toList()
|
||||
if (filters != null) return filters
|
||||
|
||||
// fallback for shortcuts created on API < 26
|
||||
val filterString = intent.getStringExtra(EXTRA_KEY_FILTERS_STRING)
|
||||
if (filterString != null) {
|
||||
return filterString.split(EXTRA_STRING_ARRAY_SEPARATOR)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun submitPickedItems(call: MethodCall) {
|
||||
val pickedUris = call.argument<List<String>>("uris")
|
||||
if (pickedUris != null && pickedUris.isNotEmpty()) {
|
||||
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(context, Uri.parse(uriString)) }
|
||||
|
@ -278,6 +318,19 @@ class MainActivity : FlutterActivity() {
|
|||
finish()
|
||||
}
|
||||
|
||||
private fun submitPickedCollectionFilters(call: MethodCall) {
|
||||
val filters = call.argument<List<String>>("filters")
|
||||
if (filters != null) {
|
||||
val intent = Intent()
|
||||
.putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray())
|
||||
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
|
||||
setResult(RESULT_OK, intent)
|
||||
} else {
|
||||
setResult(RESULT_CANCELED)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
private fun setupShortcuts() {
|
||||
// do not use 'route' as extra key, as the Flutter framework acts on it
|
||||
|
@ -291,7 +344,7 @@ class MainActivity : FlutterActivity() {
|
|||
.setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_search else R.drawable.ic_shortcut_search))
|
||||
.setIntent(
|
||||
Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
|
||||
.putExtra(SHORTCUT_KEY_PAGE, "/search")
|
||||
.putExtra(EXTRA_KEY_PAGE, "/search")
|
||||
)
|
||||
.build()
|
||||
|
||||
|
@ -300,7 +353,7 @@ class MainActivity : FlutterActivity() {
|
|||
.setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_movie else R.drawable.ic_shortcut_movie))
|
||||
.setIntent(
|
||||
Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
|
||||
.putExtra(SHORTCUT_KEY_PAGE, "/collection")
|
||||
.putExtra(EXTRA_KEY_PAGE, "/collection")
|
||||
.putExtra("filters", arrayOf("{\"type\":\"mime\",\"mime\":\"video/*\"}"))
|
||||
)
|
||||
.build()
|
||||
|
@ -314,7 +367,7 @@ class MainActivity : FlutterActivity() {
|
|||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<MainActivity>()
|
||||
const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer"
|
||||
const val INTENT_CHANNEL = "deckers.thibault/aves/intent"
|
||||
const val EXTRA_STRING_ARRAY_SEPARATOR = "###"
|
||||
const val DOCUMENT_TREE_ACCESS_REQUEST = 1
|
||||
const val OPEN_FROM_ANALYSIS_SERVICE = 2
|
||||
|
@ -322,29 +375,39 @@ class MainActivity : FlutterActivity() {
|
|||
const val OPEN_FILE_REQUEST = 4
|
||||
const val DELETE_SINGLE_PERMISSION_REQUEST = 5
|
||||
const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 6
|
||||
const val PICK_COLLECTION_FILTERS_REQUEST = 7
|
||||
|
||||
const val INTENT_DATA_KEY_ACTION = "action"
|
||||
const val INTENT_DATA_KEY_FILTERS = "filters"
|
||||
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
|
||||
const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
|
||||
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_PICK_ITEMS = "pick_items"
|
||||
const val INTENT_ACTION_PICK_COLLECTION_FILTERS = "pick_collection_filters"
|
||||
const val INTENT_ACTION_SCREEN_SAVER = "screen_saver"
|
||||
const val INTENT_ACTION_SCREEN_SAVER_SETTINGS = "screen_saver_settings"
|
||||
const val INTENT_ACTION_SEARCH = "search"
|
||||
const val INTENT_ACTION_SET_WALLPAPER = "set_wallpaper"
|
||||
const val INTENT_ACTION_VIEW = "view"
|
||||
const val INTENT_ACTION_WIDGET_OPEN = "widget_open"
|
||||
const val INTENT_ACTION_WIDGET_SETTINGS = "widget_settings"
|
||||
|
||||
const val SHORTCUT_KEY_PAGE = "page"
|
||||
const val SHORTCUT_KEY_FILTERS_ARRAY = "filters"
|
||||
const val SHORTCUT_KEY_FILTERS_STRING = "filtersString"
|
||||
const val INTENT_DATA_KEY_ACTION = "action"
|
||||
const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
|
||||
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
|
||||
val pendingStorageAccessResultHandlers = ConcurrentHashMap<Int, PendingStorageAccessResultHandler>()
|
||||
|
||||
var pendingScopedStoragePermissionCompleter: CompletableFuture<Boolean>? = null
|
||||
|
||||
var pendingCollectionFilterPickHandler: ((filters: List<String>?) -> Unit)? = null
|
||||
|
||||
private fun onStorageAccessResult(requestCode: Int, uri: Uri?) {
|
||||
Log.i(LOG_TAG, "onStorageAccessResult with requestCode=$requestCode, uri=$uri")
|
||||
val handler = pendingStorageAccessResultHandlers.remove(requestCode) ?: return
|
||||
|
|
|
@ -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 = "/"
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ import kotlin.coroutines.resume
|
|||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvider() {
|
||||
class SearchSuggestionsProvider : ContentProvider() {
|
||||
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
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> {
|
||||
if (backgroundFlutterEngine == null) {
|
||||
if (flutterEngine == null) {
|
||||
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)
|
||||
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 {
|
||||
return suspendCoroutine { cont ->
|
||||
|
@ -96,7 +104,7 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid
|
|||
}
|
||||
|
||||
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 getType(uri: Uri): String? = null
|
||||
|
@ -137,6 +135,6 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid
|
|||
const val SHARED_PREFERENCES_KEY = "platform_search"
|
||||
const val CALLBACK_HANDLE_KEY = "callback_handle"
|
||||
|
||||
private var backgroundFlutterEngine: FlutterEngine? = null
|
||||
private var flutterEngine: FlutterEngine? = null
|
||||
}
|
||||
}
|
|
@ -2,10 +2,15 @@ package deckers.thibault.aves
|
|||
|
||||
import android.content.Intent
|
||||
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 app.loup.streams_channel.StreamsChannel
|
||||
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.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.getParcelableExtraCompat
|
||||
|
@ -23,18 +28,19 @@ class WallpaperActivity : FlutterActivity() {
|
|||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val messenger = flutterEngine!!.dartExecutor.binaryMessenger
|
||||
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, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
||||
// - need Activity
|
||||
// - need ContextWrapper
|
||||
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
|
||||
MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(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
|
||||
// - need Context
|
||||
|
@ -43,7 +49,7 @@ class WallpaperActivity : FlutterActivity() {
|
|||
// intent handling
|
||||
// detail fetch: dart -> platform
|
||||
intentDataMap = extractIntentData(intent)
|
||||
MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result ->
|
||||
MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getIntentData" -> {
|
||||
result.success(intentDataMap)
|
||||
|
@ -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?> {
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
|
||||
|
@ -102,6 +98,5 @@ class WallpaperActivity : FlutterActivity() {
|
|||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<WallpaperActivity>()
|
||||
const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
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.MethodCallHandler
|
||||
|
||||
class AccessibilityHandler(private val activity: Activity) : MethodCallHandler {
|
||||
class AccessibilityHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler {
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"areAnimationsRemoved" -> safe(call, result, ::areAnimationsRemoved)
|
||||
|
@ -28,7 +28,7 @@ class AccessibilityHandler(private val activity: Activity) : MethodCallHandler {
|
|||
@SuppressLint("ObsoleteSdkInt")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
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) {
|
||||
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 content = call.argument<List<String>>("content")
|
||||
if (originalTimeoutMillis == null || content == null) {
|
||||
result.error("getRecommendedTimeoutMillis-args", "failed because of missing arguments", null)
|
||||
result.error("getRecommendedTimeoutMillis-args", "missing arguments", null)
|
||||
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) {
|
||||
result.error("getRecommendedTimeoutMillis-service", "failed to get accessibility manager", null)
|
||||
return
|
||||
|
|
|
@ -16,7 +16,10 @@ import deckers.thibault.aves.utils.ContextUtils.isMyServiceRunning
|
|||
import deckers.thibault.aves.utils.LogUtils
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
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 {
|
||||
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) {
|
||||
val callbackHandle = call.argument<Number>("callbackHandle")?.toLong()
|
||||
if (callbackHandle == null) {
|
||||
result.error("registerCallback-args", "failed because of missing arguments", null)
|
||||
result.error("registerCallback-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -47,7 +50,7 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
|
|||
private fun startAnalysis(call: MethodCall, result: MethodChannel.Result) {
|
||||
val force = call.argument<Boolean>("force")
|
||||
if (force == null) {
|
||||
result.error("startAnalysis-args", "failed because of missing arguments", null)
|
||||
result.error("startAnalysis-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -56,9 +59,9 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp
|
|||
|
||||
if (!activity.isMyServiceRunning(AnalysisService::class.java)) {
|
||||
val intent = Intent(activity, AnalysisService::class.java)
|
||||
intent.putExtra(AnalysisService.KEY_COMMAND, AnalysisService.COMMAND_START)
|
||||
intent.putExtra(AnalysisService.KEY_ENTRY_IDS, entryIds?.toIntArray())
|
||||
intent.putExtra(AnalysisService.KEY_FORCE, force)
|
||||
.putExtra(AnalysisService.KEY_COMMAND, AnalysisService.COMMAND_START)
|
||||
.putExtra(AnalysisService.KEY_ENTRY_IDS, entryIds?.toIntArray())
|
||||
.putExtra(AnalysisService.KEY_FORCE, force)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
activity.startForegroundService(intent)
|
||||
} else {
|
||||
|
|
|
@ -20,9 +20,9 @@ import com.bumptech.glide.load.DecodeFormat
|
|||
import com.bumptech.glide.request.RequestOptions
|
||||
import deckers.thibault.aves.MainActivity
|
||||
import deckers.thibault.aves.MainActivity.Companion.EXTRA_STRING_ARRAY_SEPARATOR
|
||||
import deckers.thibault.aves.MainActivity.Companion.SHORTCUT_KEY_FILTERS_ARRAY
|
||||
import deckers.thibault.aves.MainActivity.Companion.SHORTCUT_KEY_FILTERS_STRING
|
||||
import deckers.thibault.aves.MainActivity.Companion.SHORTCUT_KEY_PAGE
|
||||
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_ARRAY
|
||||
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING
|
||||
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE
|
||||
import deckers.thibault.aves.R
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
|
||||
|
@ -142,7 +142,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
val packageName = call.argument<String>("packageName")
|
||||
val sizeDip = call.argument<Number>("sizeDip")?.toDouble()
|
||||
if (packageName == null || sizeDip == null) {
|
||||
result.error("getAppIcon-args", "failed because of missing arguments", null)
|
||||
result.error("getAppIcon-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -208,7 +208,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val label = call.argument<String>("label")
|
||||
if (uri == null) {
|
||||
result.error("copyToClipboard-args", "failed because of missing arguments", null)
|
||||
result.error("copyToClipboard-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -235,7 +235,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
if (uri == null) {
|
||||
result.error("edit-args", "failed because of missing arguments", null)
|
||||
result.error("edit-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -252,7 +252,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
if (uri == null) {
|
||||
result.error("open-args", "failed because of missing arguments", null)
|
||||
result.error("open-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -267,7 +267,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
private fun openMap(call: MethodCall, result: MethodChannel.Result) {
|
||||
val geoUri = call.argument<String>("geoUri")?.let { Uri.parse(it) }
|
||||
if (geoUri == null) {
|
||||
result.error("openMap-args", "failed because of missing arguments", null)
|
||||
result.error("openMap-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -282,7 +282,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
if (uri == null) {
|
||||
result.error("setAs-args", "failed because of missing arguments", null)
|
||||
result.error("setAs-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -298,7 +298,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
val title = call.argument<String>("title")
|
||||
val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType")
|
||||
if (urisByMimeType == null) {
|
||||
result.error("setAs-args", "failed because of missing arguments", null)
|
||||
result.error("setAs-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -378,7 +378,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
val filters = call.argument<List<String>>("filters")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -407,11 +407,11 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
val intent = when {
|
||||
uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java)
|
||||
filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
|
||||
.putExtra(SHORTCUT_KEY_PAGE, "/collection")
|
||||
.putExtra(SHORTCUT_KEY_FILTERS_ARRAY, filters.toTypedArray())
|
||||
.putExtra(EXTRA_KEY_PAGE, "/collection")
|
||||
.putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray())
|
||||
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
|
||||
// so we use a joined `String` as fallback
|
||||
.putExtra(SHORTCUT_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
|
||||
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
|
||||
else -> {
|
||||
result.error("pin-intent", "failed to build intent", null)
|
||||
return
|
||||
|
|
|
@ -138,7 +138,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
|||
private fun getBitmapFactoryInfo(call: MethodCall, result: MethodChannel.Result) {
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
if (uri == null) {
|
||||
result.error("getBitmapDecoderInfo-args", "failed because of missing arguments", null)
|
||||
result.error("getBitmapDecoderInfo-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -167,7 +167,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
|||
val mimeType = call.argument<String>("mimeType")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
if (mimeType == null || uri == null) {
|
||||
result.error("getContentResolverMetadata-args", "failed because of missing arguments", null)
|
||||
result.error("getContentResolverMetadata-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -224,7 +224,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
if (mimeType == null || uri == null) {
|
||||
result.error("getExifInterfaceMetadata-args", "failed because of missing arguments", null)
|
||||
result.error("getExifInterfaceMetadata-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -250,7 +250,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
|||
private fun getMediaMetadataRetrieverMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
if (uri == null) {
|
||||
result.error("getMediaMetadataRetrieverMetadata-args", "failed because of missing arguments", null)
|
||||
result.error("getMediaMetadataRetrieverMetadata-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -276,7 +276,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
if (mimeType == null || uri == null) {
|
||||
result.error("getMetadataExtractorSummary-args", "failed because of missing arguments", null)
|
||||
result.error("getMetadataExtractorSummary-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -319,7 +319,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
|||
val mimeType = call.argument<String>("mimeType")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
if (mimeType == null || uri == null) {
|
||||
result.error("getPixyMetadata-args", "failed because of missing arguments", null)
|
||||
result.error("getPixyMetadata-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -340,7 +340,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
|||
private fun getTiffStructure(call: MethodCall, result: MethodChannel.Result) {
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
if (uri == null) {
|
||||
result.error("getTiffStructure-args", "failed because of missing arguments", null)
|
||||
result.error("getTiffStructure-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
if (mimeType == null || uri == null) {
|
||||
result.error("getExifThumbnails-args", "failed because of missing arguments", null)
|
||||
result.error("getExifThumbnails-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
|||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
val displayName = call.argument<String>("displayName")
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -108,7 +108,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val displayName = call.argument<String>("displayName")
|
||||
if (uri == null) {
|
||||
result.error("extractVideoEmbeddedPicture-args", "failed because of missing arguments", null)
|
||||
result.error("extractVideoEmbeddedPicture-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -143,7 +143,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
|||
val dataPropPath = call.argument<String>("propPath")
|
||||
val embedMimeType = call.argument<String>("propMimeType")
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ class GeocodingHandler(private val context: Context) : MethodCallHandler {
|
|||
val localeString = call.argument<String>("locale")
|
||||
val maxResults = call.argument<Int>("maxResults") ?: 1
|
||||
if (latitude == null || longitude == null) {
|
||||
result.error("getAddress-args", "failed because of missing arguments", null)
|
||||
result.error("getAddress-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ class GlobalSearchHandler(private val context: Context) : MethodCallHandler {
|
|||
private fun registerCallback(call: MethodCall, result: MethodChannel.Result) {
|
||||
val callbackHandle = call.argument<Number>("callbackHandle")?.toLong()
|
||||
if (callbackHandle == null) {
|
||||
result.error("registerCallback-args", "failed because of missing arguments", null)
|
||||
result.error("registerCallback-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>()
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.bumptech.glide.Glide
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
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.TiffRegionFetcher
|
||||
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.MimeTypes
|
||||
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.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
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 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) {
|
||||
when (call.method) {
|
||||
"getEntry" -> ioScope.launch { safe(call, result, ::getEntry) }
|
||||
"getThumbnail" -> ioScope.launch { safeSuspend(call, result, ::getThumbnail) }
|
||||
"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) }
|
||||
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 uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
if (uri == null) {
|
||||
result.error("getEntry-args", "failed because of missing arguments", null)
|
||||
result.error("getEntry-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -56,7 +53,7 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
|
|||
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 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()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter
|
||||
ThumbnailFetcher(
|
||||
activity,
|
||||
context,
|
||||
uri,
|
||||
mimeType,
|
||||
dateModifiedSecs,
|
||||
|
@ -107,20 +104,20 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
|
|||
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) {
|
||||
result.error("getRegion-args", "failed because of missing arguments", null)
|
||||
result.error("getRegion-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
val regionRect = Rect(x, y, x + width, y + height)
|
||||
when (mimeType) {
|
||||
MimeTypes.SVG -> SvgRegionFetcher(activity).fetch(
|
||||
MimeTypes.SVG -> SvgRegionFetcher(context).fetch(
|
||||
uri = uri,
|
||||
regionRect = regionRect,
|
||||
imageWidth = imageWidth,
|
||||
imageHeight = imageHeight,
|
||||
result = result,
|
||||
)
|
||||
MimeTypes.TIFF -> TiffRegionFetcher(activity).fetch(
|
||||
MimeTypes.TIFF -> TiffRegionFetcher(context).fetch(
|
||||
uri = uri,
|
||||
page = pageId ?: 0,
|
||||
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) {
|
||||
Glide.get(activity).clearDiskCache()
|
||||
Glide.get(context).clearDiskCache()
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<MediaFileHandler>()
|
||||
const val CHANNEL = "deckers.thibault/aves/media_file"
|
||||
|
||||
val cancelledOps = HashSet<String>()
|
||||
const val CHANNEL = "deckers.thibault/aves/media_fetch"
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ class MediaStoreHandler(private val context: Context) : MethodCallHandler {
|
|||
private fun checkObsoleteContentIds(call: MethodCall, result: MethodChannel.Result) {
|
||||
val knownContentIds = call.argument<List<Int?>>("knownContentIds")
|
||||
if (knownContentIds == null) {
|
||||
result.error("checkObsoleteContentIds-args", "failed because of missing arguments", null)
|
||||
result.error("checkObsoleteContentIds-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
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) {
|
||||
val knownPathById = call.argument<Map<Int?, String?>>("knownPathById")
|
||||
if (knownPathById == null) {
|
||||
result.error("checkObsoletePaths-args", "failed because of missing arguments", null)
|
||||
result.error("checkObsoletePaths-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
result.success(MediaStoreImageProvider().checkObsoletePaths(context, knownPathById))
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContextWrapper
|
||||
import android.net.Uri
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
import deckers.thibault.aves.model.ExifOrientationOp
|
||||
|
@ -15,7 +15,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.SupervisorJob
|
||||
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)
|
||||
|
||||
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) {
|
||||
val clockwise = call.argument<Boolean>("clockwise")
|
||||
if (clockwise == null) {
|
||||
result.error("rotate-args", "failed because of missing arguments", null)
|
||||
result.error("rotate-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -48,7 +48,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
|
|||
private fun editOrientation(call: MethodCall, result: MethodChannel.Result, op: ExifOrientationOp) {
|
||||
val entryMap = call.argument<FieldMap>("entry")
|
||||
if (entryMap == null) {
|
||||
result.error("editOrientation-args", "failed because of missing arguments", null)
|
||||
result.error("editOrientation-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
|
|||
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 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 entryMap = call.argument<FieldMap>("entry")
|
||||
if (entryMap == null || fields == null) {
|
||||
result.error("editDate-args", "failed because of missing arguments", null)
|
||||
result.error("editDate-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
|
|||
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 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 autoCorrectTrailerOffset = call.argument<Boolean>("autoCorrectTrailerOffset")
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -125,7 +125,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
|
|||
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 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) {
|
||||
val entryMap = call.argument<FieldMap>("entry")
|
||||
if (entryMap == null) {
|
||||
result.error("removeTrailerVideo-args", "failed because of missing arguments", null)
|
||||
result.error("removeTrailerVideo-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -152,7 +152,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
|
|||
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 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 entryMap = call.argument<FieldMap>("entry")
|
||||
if (entryMap == null || types == null) {
|
||||
result.error("removeTypes-args", "failed because of missing arguments", null)
|
||||
result.error("removeTypes-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -180,7 +180,7 @@ class MetadataEditHandler(private val activity: Activity) : MethodCallHandler {
|
|||
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 onFailure(throwable: Throwable) = result.error("removeTypes-failure", "failed to remove metadata for mimeType=$mimeType uri=$uri", throwable.message)
|
||||
})
|
||||
|
|
|
@ -115,7 +115,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
if (mimeType == null || uri == null) {
|
||||
result.error("getAllMetadata-args", "failed because of missing arguments", null)
|
||||
result.error("getAllMetadata-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -424,7 +424,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
val path = call.argument<String>("path")
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
if (mimeType == null || uri == null) {
|
||||
result.error("getCatalogMetadata-args", "failed because of missing arguments", null)
|
||||
result.error("getCatalogMetadata-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -691,7 +691,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
if (mimeType == null || uri == null) {
|
||||
result.error("getOverlayMetadata-args", "failed because of missing arguments", null)
|
||||
result.error("getOverlayMetadata-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -761,7 +761,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
if (mimeType == null || uri == null) {
|
||||
result.error("getGeoTiffInfo-args", "failed because of missing arguments", null)
|
||||
result.error("getGeoTiffInfo-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -802,7 +802,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -824,7 +824,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
if (mimeType == null || uri == null) {
|
||||
result.error("getPanoramaInfo-args", "failed because of missing arguments", null)
|
||||
result.error("getPanoramaInfo-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -863,7 +863,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
val mimeType = call.argument<String>("mimeType")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
if (mimeType == null || uri == null) {
|
||||
result.error("getIptc-args", "failed because of missing arguments", null)
|
||||
result.error("getIptc-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -888,7 +888,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
if (mimeType == null || uri == null) {
|
||||
result.error("getXmp-args", "failed because of missing arguments", null)
|
||||
result.error("getXmp-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -918,7 +918,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
private fun hasContentResolverProp(call: MethodCall, result: MethodChannel.Result) {
|
||||
val prop = call.argument<String>("prop")
|
||||
if (prop == null) {
|
||||
result.error("hasContentResolverProp-args", "failed because of missing arguments", null)
|
||||
result.error("hasContentResolverProp-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -938,7 +938,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val prop = call.argument<String>("prop")
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -992,7 +992,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
val field = call.argument<String>("field")
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
)
|
||||
)
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (e: Exception) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
"state" to EnvironmentCompat.getStorageState(volumeFile)
|
||||
)
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (e: Exception) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
private fun getFreeSpace(call: MethodCall, result: MethodChannel.Result) {
|
||||
val path = call.argument<String>("path")
|
||||
if (path == null) {
|
||||
result.error("getFreeSpace-args", "failed because of missing arguments", null)
|
||||
result.error("getFreeSpace-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -112,7 +112,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
private fun getInaccessibleDirectories(call: MethodCall, result: MethodChannel.Result) {
|
||||
val dirPaths = call.argument<List<String>>("dirPaths")
|
||||
if (dirPaths == null) {
|
||||
result.error("getInaccessibleDirectories-args", "failed because of missing arguments", null)
|
||||
result.error("getInaccessibleDirectories-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -126,7 +126,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
private fun revokeDirectoryAccess(call: MethodCall, result: MethodChannel.Result) {
|
||||
val path = call.argument<String>("path")
|
||||
if (path == null) {
|
||||
result.error("revokeDirectoryAccess-args", "failed because of missing arguments", null)
|
||||
result.error("revokeDirectoryAccess-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -142,7 +142,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
private fun deleteEmptyDirectories(call: MethodCall, result: MethodChannel.Result) {
|
||||
val dirPaths = call.argument<List<String>>("dirPaths")
|
||||
if (dirPaths == null) {
|
||||
result.error("deleteEmptyDirectories-args", "failed because of missing arguments", null)
|
||||
result.error("deleteEmptyDirectories-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -167,7 +167,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
private fun canInsertMedia(call: MethodCall, result: MethodChannel.Result) {
|
||||
val directories = call.argument<List<FieldMap>>("directories")
|
||||
if (directories == null) {
|
||||
result.error("canInsertMedia-args", "failed because of missing arguments", null)
|
||||
result.error("canInsertMedia-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.WallpaperManager
|
||||
import android.app.WallpaperManager.FLAG_LOCK
|
||||
import android.app.WallpaperManager.FLAG_SYSTEM
|
||||
import android.content.ContextWrapper
|
||||
import android.os.Build
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
|
@ -14,7 +14,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.SupervisorJob
|
||||
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)
|
||||
|
||||
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 lock = call.argument<Boolean>("lock")
|
||||
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
|
||||
}
|
||||
|
||||
val manager = WallpaperManager.getInstance(activity)
|
||||
val manager = WallpaperManager.getInstance(contextWrapper)
|
||||
val supported = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || manager.isWallpaperSupported
|
||||
val allowed = Build.VERSION.SDK_INT < Build.VERSION_CODES.N || manager.isSetWallpaperAllowed
|
||||
if (!supported || !allowed) {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -22,9 +22,9 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
// starting activity to give access with the native dialog
|
||||
// starting activity to get a result (e.g. storage access via native dialog)
|
||||
// breaks the regular `MethodChannel` so we use a stream channel instead
|
||||
class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?) : EventChannel.StreamHandler {
|
||||
class ActivityResultStreamHandler(private val activity: Activity, arguments: Any?) : EventChannel.StreamHandler {
|
||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private lateinit var eventSink: EventSink
|
||||
private lateinit var handler: Handler
|
||||
|
@ -48,6 +48,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
|||
"requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() }
|
||||
"createFile" -> ioScope.launch { createFile() }
|
||||
"openFile" -> ioScope.launch { openFile() }
|
||||
"pickCollectionFilters" -> pickCollectionFilters()
|
||||
else -> endOfStream()
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +56,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
|||
private suspend fun requestDirectoryAccess() {
|
||||
val path = args["path"] as String?
|
||||
if (path == null) {
|
||||
error("requestDirectoryAccess-args", "failed because of missing arguments", null)
|
||||
error("requestDirectoryAccess-args", "missing arguments", null)
|
||||
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 mimeTypes = (args["mimeTypes"] as List<*>?)?.mapNotNull { if (it is String) it else null }
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -111,7 +112,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
|||
val mimeType = args["mimeType"] as String?
|
||||
val bytes = args["bytes"] as ByteArray?
|
||||
if (name == null || mimeType == null || bytes == null) {
|
||||
error("createFile-args", "failed because of missing arguments", null)
|
||||
error("createFile-args", "missing arguments", null)
|
||||
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?) {}
|
||||
|
||||
private fun success(result: Any?) {
|
||||
|
@ -221,8 +234,8 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
|||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<StorageAccessStreamHandler>()
|
||||
const val CHANNEL = "deckers.thibault/aves/storage_access_stream"
|
||||
private val LOG_TAG = LogUtils.createTag<ActivityResultStreamHandler>()
|
||||
const val CHANNEL = "deckers.thibault/aves/activity_result_stream"
|
||||
private const val BUFFER_SIZE = 2 shl 17 // 256kB
|
||||
}
|
||||
}
|
|
@ -87,7 +87,7 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
|
|||
val pageId = arguments["pageId"] as Int?
|
||||
|
||||
if (mimeType == null || uri == null) {
|
||||
error("streamImage-args", "failed because of missing arguments", null)
|
||||
error("streamImage-args", "missing arguments", null)
|
||||
endOfStream()
|
||||
return
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import android.net.Uri
|
|||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
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.FieldMap
|
||||
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 nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?)
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -174,7 +174,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?)
|
||||
val rawEntryMap = arguments["entriesByDestination"] as Map<*, *>?
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -207,7 +207,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
|
||||
val rawEntryMap = arguments["entriesToNewName"] as Map<*, *>?
|
||||
if (rawEntryMap == null || rawEntryMap.isEmpty()) {
|
||||
error("rename-args", "failed because of missing arguments", null)
|
||||
error("rename-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,6 @@ class IntentStreamHandler : EventChannel.StreamHandler {
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val CHANNEL = "deckers.thibault/aves/intent"
|
||||
const val CHANNEL = "deckers.thibault/aves/new_intent_stream"
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package deckers.thibault.aves.model.provider
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
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)
|
||||
if (!file.exists()) return
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package deckers.thibault.aves.model.provider
|
|||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
|
@ -45,7 +46,7 @@ abstract class ImageProvider {
|
|||
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")
|
||||
}
|
||||
|
||||
|
@ -151,7 +152,7 @@ abstract class ImageProvider {
|
|||
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
|
||||
}
|
||||
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
|
||||
activity = activity,
|
||||
contextWrapper = activity,
|
||||
dir = targetDir,
|
||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||
mimeType = exportMimeType,
|
||||
|
@ -242,7 +243,7 @@ abstract class ImageProvider {
|
|||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun captureFrame(
|
||||
activity: Activity,
|
||||
contextWrapper: ContextWrapper,
|
||||
desiredNameWithoutExtension: String,
|
||||
exifFields: FieldMap,
|
||||
bytes: ByteArray,
|
||||
|
@ -250,7 +251,7 @@ abstract class ImageProvider {
|
|||
nameConflictStrategy: NameConflictStrategy,
|
||||
callback: ImageOpCallback,
|
||||
) {
|
||||
val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir)
|
||||
val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(contextWrapper, targetDir)
|
||||
if (!File(targetDir).exists()) {
|
||||
callback.onFailure(Exception("failed to create directory at path=$targetDir"))
|
||||
return
|
||||
|
@ -265,7 +266,7 @@ abstract class ImageProvider {
|
|||
val captureMimeType = MimeTypes.JPEG
|
||||
val targetNameWithoutExtension = try {
|
||||
resolveTargetFileNameWithoutExtension(
|
||||
activity = activity,
|
||||
contextWrapper = contextWrapper,
|
||||
dir = targetDir,
|
||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||
mimeType = captureMimeType,
|
||||
|
@ -287,7 +288,7 @@ abstract class ImageProvider {
|
|||
// through a document URI, not a tree URI
|
||||
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
|
||||
val targetTreeFile = targetDirDocFile.createFile(captureMimeType, targetNameWithoutExtension)
|
||||
val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
|
||||
val targetDocFile = DocumentFileCompat.fromSingleUri(contextWrapper, targetTreeFile.uri)
|
||||
|
||||
try {
|
||||
if (exifFields.isEmpty()) {
|
||||
|
@ -355,7 +356,7 @@ abstract class ImageProvider {
|
|||
|
||||
val fileName = targetDocFile.name
|
||||
val targetFullPath = targetDir + fileName
|
||||
val newFields = MediaStoreImageProvider().scanNewPath(activity, targetFullPath, captureMimeType)
|
||||
val newFields = MediaStoreImageProvider().scanNewPath(contextWrapper, targetFullPath, captureMimeType)
|
||||
callback.onSuccess(newFields)
|
||||
} catch (e: Exception) {
|
||||
callback.onFailure(e)
|
||||
|
@ -364,7 +365,7 @@ abstract class ImageProvider {
|
|||
|
||||
// returns available name to use, or `null` to skip it
|
||||
suspend fun resolveTargetFileNameWithoutExtension(
|
||||
activity: Activity,
|
||||
contextWrapper: ContextWrapper,
|
||||
dir: String,
|
||||
desiredNameWithoutExtension: String,
|
||||
mimeType: String,
|
||||
|
@ -386,9 +387,9 @@ abstract class ImageProvider {
|
|||
if (targetFile.exists()) {
|
||||
val path = targetFile.path
|
||||
MediaStoreImageProvider().apply {
|
||||
val uri = getContentUriForPath(activity, path)
|
||||
val uri = getContentUriForPath(contextWrapper, path)
|
||||
uri ?: throw Exception("failed to find content URI for path=$path")
|
||||
delete(activity, uri, path, mimeType)
|
||||
delete(contextWrapper, uri, path, mimeType)
|
||||
}
|
||||
}
|
||||
desiredNameWithoutExtension
|
||||
|
|
|
@ -3,10 +3,7 @@ package deckers.thibault.aves.model.provider
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.RecoverableSecurityException
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.*
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
|
@ -280,7 +277,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType
|
||||
|
||||
// `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")
|
||||
|
||||
// the following situations are possible:
|
||||
|
@ -291,10 +288,10 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
val fileExists = file.exists()
|
||||
|
||||
if (fileExists) {
|
||||
if (StorageUtils.canEditByFile(activity, path)) {
|
||||
if (hasEntry(activity, uri)) {
|
||||
if (StorageUtils.canEditByFile(contextWrapper, path)) {
|
||||
if (hasEntry(contextWrapper, uri)) {
|
||||
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 practice, the file may still be there afterwards
|
||||
|
@ -303,31 +300,31 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
if (file.delete()) {
|
||||
// in theory, scanning an obsolete path should remove the entry from the Media Store
|
||||
// in practice, the entry may still be there afterwards
|
||||
scanObsoletePath(activity, uri, path, mimeType)
|
||||
scanObsoletePath(contextWrapper, uri, path, mimeType)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else if (!isMediaUriPermissionGranted(activity, uri, mimeType)
|
||||
&& StorageUtils.requireAccessPermission(activity, path)
|
||||
} else if (!isMediaUriPermissionGranted(contextWrapper, uri, mimeType)
|
||||
&& StorageUtils.requireAccessPermission(contextWrapper, path)
|
||||
) {
|
||||
// the delete request may yield a `RecoverableSecurityException` when using scoped storage,
|
||||
// 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")
|
||||
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 practice, the file may still be there afterwards
|
||||
if (file.exists()) {
|
||||
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")
|
||||
if (df != null && df.delete()) {
|
||||
scanObsoletePath(activity, uri, path, mimeType)
|
||||
scanObsoletePath(contextWrapper, uri, path, mimeType)
|
||||
return
|
||||
}
|
||||
throw Exception("failed to delete document with df=$df")
|
||||
|
@ -343,28 +340,28 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
|
||||
try {
|
||||
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")
|
||||
}
|
||||
} catch (securityException: SecurityException) {
|
||||
// even if the app has access permission granted on the containing directory,
|
||||
// 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
|
||||
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)
|
||||
val rse = securityException as? RecoverableSecurityException ?: throw securityException
|
||||
val intentSender = rse.userAction.actionIntent.intentSender
|
||||
|
||||
// request user permission for this item
|
||||
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()
|
||||
|
||||
MainActivity.pendingScopedStoragePermissionCompleter = null
|
||||
if (granted) {
|
||||
delete(activity, uri, path, mimeType)
|
||||
delete(contextWrapper, uri, path, mimeType)
|
||||
} else {
|
||||
throw Exception("failed to get delete permission")
|
||||
}
|
||||
|
@ -494,7 +491,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
|
||||
val desiredNameWithoutExtension = desiredName.substringBeforeLast(".")
|
||||
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
|
||||
activity = activity,
|
||||
contextWrapper = activity,
|
||||
dir = targetDir,
|
||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||
mimeType = mimeType,
|
||||
|
@ -641,7 +638,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
|
||||
val dir = oldFile.parent ?: return skippedFieldMap
|
||||
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
|
||||
activity = activity,
|
||||
contextWrapper = activity,
|
||||
dir = dir,
|
||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||
mimeType = mimeType,
|
||||
|
|
|
@ -24,7 +24,6 @@ fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): Ap
|
|||
@Suppress("deprecation")
|
||||
getApplicationInfo(packageName, flags)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun PackageManager.queryIntentActivitiesCompat(intent: Intent, flags: Int): List<ResolveInfo> {
|
||||
|
|
|
@ -290,7 +290,7 @@ object StorageUtils {
|
|||
if (volume != null && uuid.equals(volume.uuid, ignoreCase = true)) {
|
||||
return volumePath
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (e: Exception) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
|
5
android/app/src/main/res/layout/app_widget.xml
Normal file
5
android/app/src/main/res/layout/app_widget.xml
Normal 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" />
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Aves</string>
|
||||
<string name="app_widget_label">Bilderrahmen</string>
|
||||
<string name="wallpaper">Hintergrundbild</string>
|
||||
<string name="search_shortcut_short_label">Suche</string>
|
||||
<string name="videos_shortcut_short_label">Videos</string>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Aves</string>
|
||||
<string name="app_widget_label">Cadre photo</string>
|
||||
<string name="wallpaper">Fond d’écran</string>
|
||||
<string name="search_shortcut_short_label">Recherche</string>
|
||||
<string name="videos_shortcut_short_label">Vidéos</string>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Aves</string>
|
||||
<string name="app_widget_label">Cornice foto</string>
|
||||
<string name="wallpaper">Sfondo</string>
|
||||
<string name="search_shortcut_short_label">Ricerca</string>
|
||||
<string name="videos_shortcut_short_label">Video</string>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Aves</string>
|
||||
<string name="app_widget_label">フォトフレーム</string>
|
||||
<string name="wallpaper">壁紙</string>
|
||||
<string name="search_shortcut_short_label">検索</string>
|
||||
<string name="videos_shortcut_short_label">動画</string>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">아베스</string>
|
||||
<string name="app_widget_label">사진 액자</string>
|
||||
<string name="wallpaper">배경화면</string>
|
||||
<string name="search_shortcut_short_label">검색</string>
|
||||
<string name="videos_shortcut_short_label">동영상</string>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Aves</string>
|
||||
<string name="app_widget_label">Porta-retratos</string>
|
||||
<string name="wallpaper">Papel de parede</string>
|
||||
<string name="search_shortcut_short_label">Procurar</string>
|
||||
<string name="videos_shortcut_short_label">Vídeos</string>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Aves</string>
|
||||
<string name="app_widget_label">相框</string>
|
||||
<string name="wallpaper">壁纸</string>
|
||||
<string name="search_shortcut_short_label">搜索</string>
|
||||
<string name="videos_shortcut_short_label">视频</string>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Aves</string>
|
||||
<string name="app_widget_label">Photo Frame</string>
|
||||
<string name="wallpaper">Wallpaper</string>
|
||||
<string name="search_shortcut_short_label">Search</string>
|
||||
<string name="videos_shortcut_short_label">Videos</string>
|
||||
|
|
12
android/app/src/main/res/xml/app_widget_info.xml
Normal file
12
android/app/src/main/res/xml/app_widget_info.xml
Normal 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" />
|
2
android/app/src/main/res/xml/screen_saver.xml
Normal file
2
android/app/src/main/res/xml/screen_saver.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<dream xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:settingsActivity="deckers.thibault.aves/deckers.thibault.aves.ScreenSaverSettingsActivity" />
|
2
android/app/src/profile/res/xml/screen_saver.xml
Normal file
2
android/app/src/profile/res/xml/screen_saver.xml
Normal 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" />
|
|
@ -1,6 +1,6 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.0'
|
||||
ext.kotlin_version = '1.7.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
@ -10,8 +10,8 @@ buildscript {
|
|||
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
// GMS & Firebase Crashlytics (used by some flavors only)
|
||||
classpath 'com.google.gms:google-services:4.3.10'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.0'
|
||||
classpath 'com.google.gms:google-services:4.3.13'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.1'
|
||||
// HMS (used by some flavors only)
|
||||
classpath 'com.huawei.agconnect:agcp:1.5.2.300'
|
||||
}
|
||||
|
|
|
@ -3,3 +3,4 @@ storePassword=<KEYSTORE_PASSWORD>
|
|||
keyAlias=<KEY_ALIAS>
|
||||
keyPassword=<KEY_PASSWORD>
|
||||
googleApiKey=<GOOGLE_API_KEY>
|
||||
huaweiApiKey=<HUAWEI_API_KEY>
|
||||
|
|
5
fastlane/metadata/android/en-US/changelogs/1076.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/1076.txt
Normal 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
|
|
@ -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.
|
||||
|
||||
<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>.
|
|
@ -1,22 +1,34 @@
|
|||
enum AppMode {
|
||||
main,
|
||||
pickCollectionFiltersExternal,
|
||||
pickSingleMediaExternal,
|
||||
pickMultipleMediaExternal,
|
||||
pickMediaInternal,
|
||||
pickFilterInternal,
|
||||
screenSaver,
|
||||
setWallpaper,
|
||||
slideshow,
|
||||
view,
|
||||
}
|
||||
|
||||
extension ExtraAppMode on AppMode {
|
||||
bool get canSearch => this == AppMode.main || this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal;
|
||||
bool get canNavigate => {
|
||||
AppMode.main,
|
||||
AppMode.pickCollectionFiltersExternal,
|
||||
AppMode.pickSingleMediaExternal,
|
||||
AppMode.pickMultipleMediaExternal,
|
||||
}.contains(this);
|
||||
|
||||
bool get canSelectMedia => this == AppMode.main || this == AppMode.pickMultipleMediaExternal;
|
||||
bool get canSelectMedia => {
|
||||
AppMode.main,
|
||||
AppMode.pickMultipleMediaExternal,
|
||||
}.contains(this);
|
||||
|
||||
bool get canSelectFilter => this == AppMode.main;
|
||||
|
||||
bool get hasDrawer => this == AppMode.main || this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal;
|
||||
|
||||
bool get isPickingMedia => this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal || this == AppMode.pickMediaInternal;
|
||||
bool get isPickingMedia => {
|
||||
AppMode.pickSingleMediaExternal,
|
||||
AppMode.pickMultipleMediaExternal,
|
||||
AppMode.pickMediaInternal,
|
||||
}.contains(this);
|
||||
}
|
||||
|
|
|
@ -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:equatable/equatable.dart';
|
||||
|
@ -27,7 +27,7 @@ class AppIconImage extends ImageProvider<AppIconImageKey> {
|
|||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(AppIconImageKey key, DecoderCallback decode) {
|
||||
ImageStreamCompleter loadBuffer(AppIconImageKey key, DecoderBufferCallback decode) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode),
|
||||
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 {
|
||||
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) {
|
||||
debugPrint('$runtimeType _loadAsync failed with packageName=$packageName, error=$error');
|
||||
throw StateError('$packageName app icon decoding failed');
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui show Codec;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
@ -18,7 +18,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
|||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(RegionProviderKey key, DecoderCallback decode) {
|
||||
ImageStreamCompleter loadBuffer(RegionProviderKey key, DecoderBufferCallback decode) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode),
|
||||
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 mimeType = key.mimeType;
|
||||
final pageId = key.pageId;
|
||||
try {
|
||||
final bytes = await mediaFileService.getRegion(
|
||||
final bytes = await mediaFetchService.getRegion(
|
||||
uri,
|
||||
mimeType,
|
||||
key.rotationDegrees,
|
||||
|
@ -47,7 +47,8 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
|||
if (bytes.isEmpty) {
|
||||
throw StateError('$uri ($mimeType) region loading failed');
|
||||
}
|
||||
return await decode(bytes);
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
|
||||
return await decode(buffer);
|
||||
} catch (error) {
|
||||
// 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');
|
||||
|
@ -57,11 +58,11 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
|||
|
||||
@override
|
||||
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, RegionProviderKey key, ImageErrorListener handleError) {
|
||||
mediaFileService.resumeLoading(key);
|
||||
mediaFetchService.resumeLoading(key);
|
||||
super.resolveStreamForKey(configuration, stream, key, handleError);
|
||||
}
|
||||
|
||||
void pause() => mediaFileService.cancelRegion(key);
|
||||
void pause() => mediaFetchService.cancelRegion(key);
|
||||
}
|
||||
|
||||
@immutable
|
||||
|
|
|
@ -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:equatable/equatable.dart';
|
||||
|
@ -19,7 +19,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
|||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(ThumbnailProviderKey key, DecoderCallback decode) {
|
||||
ImageStreamCompleter loadBuffer(ThumbnailProviderKey key, DecoderBufferCallback decode) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode),
|
||||
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 mimeType = key.mimeType;
|
||||
final pageId = key.pageId;
|
||||
try {
|
||||
final bytes = await mediaFileService.getThumbnail(
|
||||
final bytes = await mediaFetchService.getThumbnail(
|
||||
uri: uri,
|
||||
mimeType: mimeType,
|
||||
pageId: pageId,
|
||||
|
@ -48,7 +48,8 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
|||
if (bytes.isEmpty) {
|
||||
throw StateError('$uri ($mimeType) loading failed');
|
||||
}
|
||||
return await decode(bytes);
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
|
||||
return await decode(buffer);
|
||||
} catch (error) {
|
||||
// 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');
|
||||
|
@ -58,11 +59,11 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
|||
|
||||
@override
|
||||
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, ImageErrorListener handleError) {
|
||||
mediaFileService.resumeLoading(key);
|
||||
mediaFetchService.resumeLoading(key);
|
||||
super.resolveStreamForKey(configuration, stream, key, handleError);
|
||||
}
|
||||
|
||||
void pause() => mediaFileService.cancelThumbnail(key);
|
||||
void pause() => mediaFetchService.cancelThumbnail(key);
|
||||
}
|
||||
|
||||
@immutable
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui' as ui show Codec;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
@ -32,7 +32,7 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
|
|||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(UriImage key, DecoderCallback decode) {
|
||||
ImageStreamCompleter loadBuffer(UriImage key, DecoderBufferCallback decode) {
|
||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||
|
||||
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);
|
||||
|
||||
try {
|
||||
final bytes = await mediaFileService.getImage(
|
||||
final bytes = await mediaFetchService.getImage(
|
||||
uri,
|
||||
mimeType,
|
||||
rotationDegrees,
|
||||
|
@ -66,7 +66,8 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
|
|||
if (bytes.isEmpty) {
|
||||
throw StateError('$uri ($mimeType) loading failed');
|
||||
}
|
||||
return await decode(bytes);
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
|
||||
return await decode(buffer);
|
||||
} catch (error) {
|
||||
// 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');
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "Anzeigen",
|
||||
"hideTooltip": "Ausblenden",
|
||||
"actionRemove": "Entfernen",
|
||||
"resetButtonTooltip": "Zurücksetzen",
|
||||
"resetTooltip": "Zurücksetzen",
|
||||
"saveTooltip": "Speichern",
|
||||
|
||||
"doubleBackExitMessage": "Zum Verlassen erneut auf „Zurück“ tippen.",
|
||||
"doNotAskAgain": "Nicht noch einmal fragen",
|
||||
|
@ -94,6 +95,7 @@
|
|||
"filterFavouriteLabel": "Favorit",
|
||||
"filterLocationEmptyLabel": "Ungeortet",
|
||||
"filterTagEmptyLabel": "Unmarkiert",
|
||||
"filterOnThisDayLabel": "Am heutigen Tag",
|
||||
"filterRatingUnratedLabel": "Nicht bewertet",
|
||||
"filterRatingRejectedLabel": "Verworfen",
|
||||
"filterTypeAnimatedLabel": "Animationen",
|
||||
|
@ -303,7 +305,6 @@
|
|||
|
||||
"aboutBug": "Fehlerbericht",
|
||||
"aboutBugSaveLogInstruction": "Anwendungsprotokolle in einer Datei speichern",
|
||||
"aboutBugSaveLogButton": "Speichern",
|
||||
"aboutBugCopyInfoInstruction": "Systeminformationen kopieren",
|
||||
"aboutBugCopyInfoButton": "Kopieren",
|
||||
"aboutBugReportInstruction": "Bericht auf GitHub mit den Protokollen und Systeminformationen",
|
||||
|
@ -417,6 +418,7 @@
|
|||
|
||||
"searchCollectionFieldHint": "Sammlung durchsuchen",
|
||||
"searchSectionRecent": "Neueste",
|
||||
"searchSectionDate": "Datum",
|
||||
"searchSectionAlbums": "Alben",
|
||||
"searchSectionCountries": "Länder",
|
||||
"searchSectionPlaces": "Orte",
|
||||
|
@ -502,6 +504,7 @@
|
|||
"settingsViewerSlideshowTitle": "Diashow",
|
||||
"settingsSlideshowRepeat": "Wiederholung",
|
||||
"settingsSlideshowShuffle": "Mischen",
|
||||
"settingsSlideshowFillScreen": "Bildschirm ausfüllen",
|
||||
"settingsSlideshowTransitionTile": "Übergang",
|
||||
"settingsSlideshowTransitionTitle": "Übergang",
|
||||
"settingsSlideshowIntervalTile": "Intervall",
|
||||
|
@ -584,6 +587,11 @@
|
|||
"settingsUnitSystemTile": "Einheiten",
|
||||
"settingsUnitSystemTitle": "Einheiten",
|
||||
|
||||
"settingsScreenSaverPageTitle": "Bildschirmschoner",
|
||||
|
||||
"settingsWidgetPageTitle": "Bilderrahmen",
|
||||
"settingsWidgetShowOutline": "Gliederung",
|
||||
|
||||
"statsPageTitle": "Statistiken",
|
||||
"statsWithGps": "{count, plural, =1{1 Element mit Standort} other{{count} Elemente mit Standort}}",
|
||||
"statsTopCountries": "Top-Länder",
|
||||
|
|
|
@ -53,7 +53,8 @@
|
|||
"showTooltip": "Show",
|
||||
"hideTooltip": "Hide",
|
||||
"actionRemove": "Remove",
|
||||
"resetButtonTooltip": "Reset",
|
||||
"resetTooltip": "Reset",
|
||||
"saveTooltip": "Save",
|
||||
|
||||
"doubleBackExitMessage": "Tap “back” again to exit.",
|
||||
"doNotAskAgain": "Do not ask again",
|
||||
|
@ -122,6 +123,7 @@
|
|||
"filterFavouriteLabel": "Favorite",
|
||||
"filterLocationEmptyLabel": "Unlocated",
|
||||
"filterTagEmptyLabel": "Untagged",
|
||||
"filterOnThisDayLabel": "On this day",
|
||||
"filterRatingUnratedLabel": "Unrated",
|
||||
"filterRatingRejectedLabel": "Rejected",
|
||||
"filterTypeAnimatedLabel": "Animated",
|
||||
|
@ -433,7 +435,6 @@
|
|||
|
||||
"aboutBug": "Bug Report",
|
||||
"aboutBugSaveLogInstruction": "Save app logs to a file",
|
||||
"aboutBugSaveLogButton": "Save",
|
||||
"aboutBugCopyInfoInstruction": "Copy system information",
|
||||
"aboutBugCopyInfoButton": "Copy",
|
||||
"aboutBugReportInstruction": "Report on GitHub with the logs and system information",
|
||||
|
@ -597,6 +598,7 @@
|
|||
|
||||
"searchCollectionFieldHint": "Search collection",
|
||||
"searchSectionRecent": "Recent",
|
||||
"searchSectionDate": "Date",
|
||||
"searchSectionAlbums": "Albums",
|
||||
"searchSectionCountries": "Countries",
|
||||
"searchSectionPlaces": "Places",
|
||||
|
@ -682,6 +684,7 @@
|
|||
"settingsViewerSlideshowTitle": "Slideshow",
|
||||
"settingsSlideshowRepeat": "Repeat",
|
||||
"settingsSlideshowShuffle": "Shuffle",
|
||||
"settingsSlideshowFillScreen": "Fill screen",
|
||||
"settingsSlideshowTransitionTile": "Transition",
|
||||
"settingsSlideshowTransitionTitle": "Transition",
|
||||
"settingsSlideshowIntervalTile": "Interval",
|
||||
|
@ -764,6 +767,11 @@
|
|||
"settingsUnitSystemTile": "Units",
|
||||
"settingsUnitSystemTitle": "Units",
|
||||
|
||||
"settingsScreenSaverPageTitle": "Screen Saver",
|
||||
|
||||
"settingsWidgetPageTitle": "Photo Frame",
|
||||
"settingsWidgetShowOutline": "Outline",
|
||||
|
||||
"statsPageTitle": "Stats",
|
||||
"statsWithGps": "{count, plural, =1{1 item with location} other{{count} items with location}}",
|
||||
"@statsWithGps": {
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "Mostrar",
|
||||
"hideTooltip": "Ocultar",
|
||||
"actionRemove": "Remover",
|
||||
"resetButtonTooltip": "Restablecer",
|
||||
"resetTooltip": "Restablecer",
|
||||
"saveTooltip": "Guardar",
|
||||
|
||||
"doubleBackExitMessage": "Presione «atrás» nuevamente para salir.",
|
||||
"doNotAskAgain": "No preguntar nuevamente",
|
||||
|
@ -303,7 +304,6 @@
|
|||
|
||||
"aboutBug": "Reporte de errores",
|
||||
"aboutBugSaveLogInstruction": "Guardar registros de la aplicación a un archivo",
|
||||
"aboutBugSaveLogButton": "Guardar",
|
||||
"aboutBugCopyInfoInstruction": "Copiar información del sistema",
|
||||
"aboutBugCopyInfoButton": "Copiar",
|
||||
"aboutBugReportInstruction": "Reportar en GitHub con los registros y la información del sistema",
|
||||
|
@ -417,6 +417,7 @@
|
|||
|
||||
"searchCollectionFieldHint": "Buscar en colección",
|
||||
"searchSectionRecent": "Reciente",
|
||||
"searchSectionDate": "Fecha",
|
||||
"searchSectionAlbums": "Álbumes",
|
||||
"searchSectionCountries": "Países",
|
||||
"searchSectionPlaces": "Lugares",
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "Afficher",
|
||||
"hideTooltip": "Masquer",
|
||||
"actionRemove": "Supprimer",
|
||||
"resetButtonTooltip": "Réinitialiser",
|
||||
"resetTooltip": "Réinitialiser",
|
||||
"saveTooltip": "Sauvegarder",
|
||||
|
||||
"doubleBackExitMessage": "Pressez «\u00A0retour\u00A0» à nouveau pour quitter.",
|
||||
"doNotAskAgain": "Ne pas demander de nouveau",
|
||||
|
@ -94,6 +95,7 @@
|
|||
"filterFavouriteLabel": "Favori",
|
||||
"filterLocationEmptyLabel": "Sans lieu",
|
||||
"filterTagEmptyLabel": "Sans libellé",
|
||||
"filterOnThisDayLabel": "Ce jour-là",
|
||||
"filterRatingUnratedLabel": "Sans notation",
|
||||
"filterRatingRejectedLabel": "Rejeté",
|
||||
"filterTypeAnimatedLabel": "Animation",
|
||||
|
@ -303,7 +305,6 @@
|
|||
|
||||
"aboutBug": "Rapports d’erreur",
|
||||
"aboutBugSaveLogInstruction": "Sauvegarder les logs de l’app vers un fichier",
|
||||
"aboutBugSaveLogButton": "Sauvegarder",
|
||||
"aboutBugCopyInfoInstruction": "Copier les informations d’environnement",
|
||||
"aboutBugCopyInfoButton": "Copier",
|
||||
"aboutBugReportInstruction": "Créer une «\u00A0issue\u00A0» sur GitHub en attachant les logs et informations d’environnement",
|
||||
|
@ -417,6 +418,7 @@
|
|||
|
||||
"searchCollectionFieldHint": "Recherche",
|
||||
"searchSectionRecent": "Historique",
|
||||
"searchSectionDate": "Date",
|
||||
"searchSectionAlbums": "Albums",
|
||||
"searchSectionCountries": "Pays",
|
||||
"searchSectionPlaces": "Lieux",
|
||||
|
@ -502,6 +504,7 @@
|
|||
"settingsViewerSlideshowTitle": "Diaporama",
|
||||
"settingsSlideshowRepeat": "Répéter",
|
||||
"settingsSlideshowShuffle": "Aléatoire",
|
||||
"settingsSlideshowFillScreen": "Remplir l’écran",
|
||||
"settingsSlideshowTransitionTile": "Transition",
|
||||
"settingsSlideshowTransitionTitle": "Transition",
|
||||
"settingsSlideshowIntervalTile": "Intervalle",
|
||||
|
@ -574,7 +577,7 @@
|
|||
"settingsThemeBrightness": "Thème",
|
||||
"settingsThemeColorHighlights": "Surlignages colorés",
|
||||
"settingsThemeEnableDynamicColor": "Couleur dynamique",
|
||||
"settingsDisplayRefreshRateModeTile": "Fréquence d’actualisation de l'écran",
|
||||
"settingsDisplayRefreshRateModeTile": "Fréquence d’actualisation de l’écran",
|
||||
"settingsDisplayRefreshRateModeTitle": "Fréquence d’actualisation",
|
||||
|
||||
"settingsSectionLanguage": "Langue & Formats",
|
||||
|
@ -584,6 +587,11 @@
|
|||
"settingsUnitSystemTile": "Unités",
|
||||
"settingsUnitSystemTitle": "Unités",
|
||||
|
||||
"settingsScreenSaverPageTitle": "Économiseur d’écran",
|
||||
|
||||
"settingsWidgetPageTitle": "Cadre photo",
|
||||
"settingsWidgetShowOutline": "Contours",
|
||||
|
||||
"statsPageTitle": "Statistiques",
|
||||
"statsWithGps": "{count, plural, =1{1 élément localisé} other{{count} éléments localisés}}",
|
||||
"statsTopCountries": "Top pays",
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "Tampilkan",
|
||||
"hideTooltip": "Sembunyikan",
|
||||
"actionRemove": "Hapus",
|
||||
"resetButtonTooltip": "Ulang",
|
||||
"resetTooltip": "Ulang",
|
||||
"saveTooltip": "Simpan",
|
||||
|
||||
"doubleBackExitMessage": "Ketuk “kembali” lagi untuk keluar.",
|
||||
"doNotAskAgain": "Jangan tanya lagi",
|
||||
|
@ -81,6 +82,9 @@
|
|||
"videoActionSetSpeed": "Kecepatan pemutaran",
|
||||
"videoActionSettings": "Pengaturan",
|
||||
|
||||
"slideshowActionResume": "Lanjutkan",
|
||||
"slideshowActionShowInCollection": "Tampilkan di Koleksi",
|
||||
|
||||
"entryInfoActionEditDate": "Ubah tanggal & waktu",
|
||||
"entryInfoActionEditLocation": "Ubah lokasi",
|
||||
"entryInfoActionEditRating": "Ubah nilai",
|
||||
|
@ -145,10 +149,23 @@
|
|||
"displayRefreshRatePreferHighest": "Penyegaran tertinggi",
|
||||
"displayRefreshRatePreferLowest": "Penyegaran terendah",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "Lewati",
|
||||
"slideshowVideoPlaybackMuted": "Mainkan bisu",
|
||||
"slideshowVideoPlaybackWithSound": "Mainkan dengan suara",
|
||||
|
||||
"themeBrightnessLight": "Terang",
|
||||
"themeBrightnessDark": "Gelap",
|
||||
"themeBrightnessBlack": "Hitam",
|
||||
|
||||
"viewerTransitionSlide": "Menggeser",
|
||||
"viewerTransitionParallax": "Paralaks",
|
||||
"viewerTransitionFade": "Memudar",
|
||||
"viewerTransitionZoomIn": "Membesar",
|
||||
|
||||
"wallpaperTargetHome": "Tampilan depan",
|
||||
"wallpaperTargetLock": "Tampilan kunci",
|
||||
"wallpaperTargetHomeLock": "Tampilan depan dan kunci",
|
||||
|
||||
"albumTierNew": "Baru",
|
||||
"albumTierPinned": "Disemat",
|
||||
"albumTierSpecial": "Biasa",
|
||||
|
@ -263,6 +280,7 @@
|
|||
"menuActionSelectAll": "Pilih semua",
|
||||
"menuActionSelectNone": "Pilih tidak ada",
|
||||
"menuActionMap": "Peta",
|
||||
"menuActionSlideshow": "Tampilan slide",
|
||||
"menuActionStats": "Statistik",
|
||||
|
||||
"viewDialogTabSort": "Sortir",
|
||||
|
@ -286,7 +304,6 @@
|
|||
|
||||
"aboutBug": "Lapor Bug",
|
||||
"aboutBugSaveLogInstruction": "Simpan log aplikasi ke file",
|
||||
"aboutBugSaveLogButton": "Simpan",
|
||||
"aboutBugCopyInfoInstruction": "Salin informasi sistem",
|
||||
"aboutBugCopyInfoButton": "Salin",
|
||||
"aboutBugReportInstruction": "Laporkan ke GitHub dengan log dan informasi sistem",
|
||||
|
@ -350,6 +367,7 @@
|
|||
"collectionEmptyFavourites": "Tidak ada favorit",
|
||||
"collectionEmptyVideos": "Tidak ada video",
|
||||
"collectionEmptyImages": "Tidak ada gambar",
|
||||
"collectionEmptyGrantAccessButtonLabel": "Berikan akses",
|
||||
|
||||
"collectionSelectSectionTooltip": "Pilih bagian",
|
||||
"collectionDeselectSectionTooltip": "Batalkan pilihan bagian",
|
||||
|
@ -399,6 +417,7 @@
|
|||
|
||||
"searchCollectionFieldHint": "Cari koleksi",
|
||||
"searchSectionRecent": "Terkini",
|
||||
"searchSectionDate": "Tanggal",
|
||||
"searchSectionAlbums": "Album",
|
||||
"searchSectionCountries": "Negara",
|
||||
"searchSectionPlaces": "Tempat",
|
||||
|
@ -480,6 +499,17 @@
|
|||
"settingsViewerShowOverlayThumbnails": "Tampilkan thumbnail",
|
||||
"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",
|
||||
"settingsSectionVideo": "Video",
|
||||
"settingsVideoShowVideos": "Tampilkan video",
|
||||
|
@ -544,6 +574,7 @@
|
|||
"settingsSectionDisplay": "Tampilan",
|
||||
"settingsThemeBrightness": "Tema",
|
||||
"settingsThemeColorHighlights": "Highlight warna",
|
||||
"settingsThemeEnableDynamicColor": "Warna dinamis",
|
||||
"settingsDisplayRefreshRateModeTile": "Tingkat penyegaran tampilan",
|
||||
"settingsDisplayRefreshRateModeTitle": "Tingkat Penyegaran",
|
||||
|
||||
|
@ -561,6 +592,7 @@
|
|||
"statsTopTags": "Label Teratas",
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "BUKA PANORAMA",
|
||||
"viewerSetWallpaperButtonLabel": "TETAPKAN SEBAGAI WALLPAPER",
|
||||
"viewerErrorUnknown": "Ups!",
|
||||
"viewerErrorDoesNotExist": "File tidak ada lagi.",
|
||||
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "Mostra",
|
||||
"hideTooltip": "Nascondi",
|
||||
"actionRemove": "Rimuovi",
|
||||
"resetButtonTooltip": "Reimposta",
|
||||
"resetTooltip": "Reimposta",
|
||||
"saveTooltip": "Salva",
|
||||
|
||||
"doubleBackExitMessage": "Tocca di nuovo «indietro» per uscire",
|
||||
"doNotAskAgain": "Non chiedere di nuovo",
|
||||
|
@ -94,6 +95,7 @@
|
|||
"filterFavouriteLabel": "Preferiti",
|
||||
"filterLocationEmptyLabel": "Senza posizione",
|
||||
"filterTagEmptyLabel": "Senza etichetta",
|
||||
"filterOnThisDayLabel": "In questo giorno",
|
||||
"filterRatingUnratedLabel": "Non valutato",
|
||||
"filterRatingRejectedLabel": "Rifiutato",
|
||||
"filterTypeAnimatedLabel": "Animato",
|
||||
|
@ -303,7 +305,6 @@
|
|||
|
||||
"aboutBug": "Segnalazione bug",
|
||||
"aboutBugSaveLogInstruction": "Salva i log dell’app in un file",
|
||||
"aboutBugSaveLogButton": "Salva",
|
||||
"aboutBugCopyInfoInstruction": "Copia le informazioni di sistema",
|
||||
"aboutBugCopyInfoButton": "Copia",
|
||||
"aboutBugReportInstruction": "Segnala su GitHub con i log e le informazioni di sistema",
|
||||
|
@ -417,6 +418,7 @@
|
|||
|
||||
"searchCollectionFieldHint": "Cerca raccolta",
|
||||
"searchSectionRecent": "Recenti",
|
||||
"searchSectionDate": "Data",
|
||||
"searchSectionAlbums": "Album",
|
||||
"searchSectionCountries": "Paesi",
|
||||
"searchSectionPlaces": "Luoghi",
|
||||
|
@ -502,6 +504,7 @@
|
|||
"settingsViewerSlideshowTitle": "Presentazione",
|
||||
"settingsSlideshowRepeat": "Ripeti",
|
||||
"settingsSlideshowShuffle": "Ordine casuale",
|
||||
"settingsSlideshowFillScreen": "Riempi schermo",
|
||||
"settingsSlideshowTransitionTile": "Transizione",
|
||||
"settingsSlideshowTransitionTitle": "Transizione",
|
||||
"settingsSlideshowIntervalTile": "Intervallo",
|
||||
|
@ -584,6 +587,11 @@
|
|||
"settingsUnitSystemTile": "Unità",
|
||||
"settingsUnitSystemTitle": "Unità",
|
||||
|
||||
"settingsScreenSaverPageTitle": "Salvaschermo",
|
||||
|
||||
"settingsWidgetPageTitle": "Cornice foto",
|
||||
"settingsWidgetShowOutline": "Contorno",
|
||||
|
||||
"statsPageTitle": "Statistiche",
|
||||
"statsWithGps": "{count, plural, =1{1 elemento con posizione} other{{count} elementi con posizione}}",
|
||||
"statsTopCountries": "Paesi più frequenti",
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "表示する",
|
||||
"hideTooltip": "非表示にする",
|
||||
"actionRemove": "削除",
|
||||
"resetButtonTooltip": "リセット",
|
||||
"resetTooltip": "リセット",
|
||||
"saveTooltip": "保存",
|
||||
|
||||
"doubleBackExitMessage": "終了するには「戻る」をもう一度タップしてください。",
|
||||
"doNotAskAgain": "今後このメッセージを表示しない",
|
||||
|
@ -81,6 +82,9 @@
|
|||
"videoActionSetSpeed": "再生速度",
|
||||
"videoActionSettings": "設定",
|
||||
|
||||
"slideshowActionResume": "再開",
|
||||
"slideshowActionShowInCollection": "コレクションで表示",
|
||||
|
||||
"entryInfoActionEditDate": "日時を編集",
|
||||
"entryInfoActionEditLocation": "位置情報を編集",
|
||||
"entryInfoActionEditRating": "評価を編集",
|
||||
|
@ -91,6 +95,7 @@
|
|||
"filterFavouriteLabel": "お気に入り",
|
||||
"filterLocationEmptyLabel": "位置情報なし",
|
||||
"filterTagEmptyLabel": "タグ情報なし",
|
||||
"filterOnThisDayLabel": "過去のこの日",
|
||||
"filterRatingUnratedLabel": "評価情報なし",
|
||||
"filterRatingRejectedLabel": "拒否",
|
||||
"filterTypeAnimatedLabel": "アニメーション",
|
||||
|
@ -145,10 +150,23 @@
|
|||
"displayRefreshRatePreferHighest": "高レート",
|
||||
"displayRefreshRatePreferLowest": "低レート",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "スキップ",
|
||||
"slideshowVideoPlaybackMuted": "ミュート再生",
|
||||
"slideshowVideoPlaybackWithSound": "音声あり再生",
|
||||
|
||||
"themeBrightnessLight": "ライト",
|
||||
"themeBrightnessDark": "ダーク",
|
||||
"themeBrightnessBlack": "ブラック",
|
||||
|
||||
"viewerTransitionSlide": "スライド",
|
||||
"viewerTransitionParallax": "パララックス",
|
||||
"viewerTransitionFade": "フェード",
|
||||
"viewerTransitionZoomIn": "ズームイン",
|
||||
|
||||
"wallpaperTargetHome": "ホーム画面",
|
||||
"wallpaperTargetLock": "ロック画面",
|
||||
"wallpaperTargetHomeLock": "ホームおよびロック画面",
|
||||
|
||||
"albumTierNew": "新規",
|
||||
"albumTierPinned": "固定",
|
||||
"albumTierSpecial": "全体",
|
||||
|
@ -263,6 +281,7 @@
|
|||
"menuActionSelectAll": "すべて選択",
|
||||
"menuActionSelectNone": "選択を解除",
|
||||
"menuActionMap": "地図",
|
||||
"menuActionSlideshow": "スライドショー",
|
||||
"menuActionStats": "統計",
|
||||
|
||||
"viewDialogTabSort": "並べ替え",
|
||||
|
@ -286,7 +305,6 @@
|
|||
|
||||
"aboutBug": "バグの報告",
|
||||
"aboutBugSaveLogInstruction": "アプリのログをファイルに保存",
|
||||
"aboutBugSaveLogButton": "保存",
|
||||
"aboutBugCopyInfoInstruction": "システム情報をコピー",
|
||||
"aboutBugCopyInfoButton": "コピー",
|
||||
"aboutBugReportInstruction": "ログとシステム情報とともに GitHub で報告",
|
||||
|
@ -350,6 +368,7 @@
|
|||
"collectionEmptyFavourites": "お気に入りはありません",
|
||||
"collectionEmptyVideos": "動画はありません",
|
||||
"collectionEmptyImages": "画像はありません",
|
||||
"collectionEmptyGrantAccessButtonLabel": "アクセスを許可",
|
||||
|
||||
"collectionSelectSectionTooltip": "セクションを選択",
|
||||
"collectionDeselectSectionTooltip": "セクションの選択を解除",
|
||||
|
@ -399,6 +418,7 @@
|
|||
|
||||
"searchCollectionFieldHint": "コレクションを検索",
|
||||
"searchSectionRecent": "最近",
|
||||
"searchSectionDate": "日付",
|
||||
"searchSectionAlbums": "アルバム",
|
||||
"searchSectionCountries": "国",
|
||||
"searchSectionPlaces": "場所",
|
||||
|
@ -480,6 +500,18 @@
|
|||
"settingsViewerShowOverlayThumbnails": "サムネイルを表示",
|
||||
"settingsViewerEnableOverlayBlurEffect": "ぼかし効果",
|
||||
|
||||
"settingsViewerSlideshowTile": "スライドショー",
|
||||
"settingsViewerSlideshowTitle": "スライドショー",
|
||||
"settingsSlideshowRepeat": "繰り返し",
|
||||
"settingsSlideshowShuffle": "シャッフル",
|
||||
"settingsSlideshowFillScreen": "画面いっぱいに表示",
|
||||
"settingsSlideshowTransitionTile": "トランジション",
|
||||
"settingsSlideshowTransitionTitle": "トランジション",
|
||||
"settingsSlideshowIntervalTile": "間隔",
|
||||
"settingsSlideshowIntervalTitle": "間隔",
|
||||
"settingsSlideshowVideoPlaybackTile": "動画を再生",
|
||||
"settingsSlideshowVideoPlaybackTitle": "動画再生",
|
||||
|
||||
"settingsVideoPageTitle": "動画設定",
|
||||
"settingsSectionVideo": "動画",
|
||||
"settingsVideoShowVideos": "動画を表示",
|
||||
|
@ -544,6 +576,7 @@
|
|||
"settingsSectionDisplay": "ディスプレイ",
|
||||
"settingsThemeBrightness": "テーマ",
|
||||
"settingsThemeColorHighlights": "カラー強調表示",
|
||||
"settingsThemeEnableDynamicColor": "ダイナミックカラー",
|
||||
"settingsDisplayRefreshRateModeTile": "ディスプレイ リフレッシュ レート",
|
||||
"settingsDisplayRefreshRateModeTitle": "リフレッシュ レート",
|
||||
|
||||
|
@ -554,6 +587,11 @@
|
|||
"settingsUnitSystemTile": "単位",
|
||||
"settingsUnitSystemTitle": "単位",
|
||||
|
||||
"settingsScreenSaverPageTitle": "スクリーンセーバー",
|
||||
|
||||
"settingsWidgetPageTitle": "フォトフレーム",
|
||||
"settingsWidgetShowOutline": "枠",
|
||||
|
||||
"statsPageTitle": "統計",
|
||||
"statsWithGps": "{count, plural, other{位置情報のあるアイテム {count} 件}}",
|
||||
"statsTopCountries": "上位の国",
|
||||
|
@ -561,6 +599,7 @@
|
|||
"statsTopTags": "上位のタグ",
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "パノラマを開く",
|
||||
"viewerSetWallpaperButtonLabel": "壁紙設定",
|
||||
"viewerErrorUnknown": "エラー",
|
||||
"viewerErrorDoesNotExist": "ファイルが存在しません。",
|
||||
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "보기",
|
||||
"hideTooltip": "숨기기",
|
||||
"actionRemove": "제거",
|
||||
"resetButtonTooltip": "복원",
|
||||
"resetTooltip": "복원",
|
||||
"saveTooltip": "저장",
|
||||
|
||||
"doubleBackExitMessage": "종료하려면 한번 더 누르세요.",
|
||||
"doNotAskAgain": "다시 묻지 않기",
|
||||
|
@ -94,6 +95,7 @@
|
|||
"filterFavouriteLabel": "즐겨찾기",
|
||||
"filterLocationEmptyLabel": "장소 없음",
|
||||
"filterTagEmptyLabel": "태그 없음",
|
||||
"filterOnThisDayLabel": "이 날",
|
||||
"filterRatingUnratedLabel": "별점 없음",
|
||||
"filterRatingRejectedLabel": "거부됨",
|
||||
"filterTypeAnimatedLabel": "애니메이션",
|
||||
|
@ -303,7 +305,6 @@
|
|||
|
||||
"aboutBug": "버그 보고",
|
||||
"aboutBugSaveLogInstruction": "앱 로그를 파일에 저장하기",
|
||||
"aboutBugSaveLogButton": "저장",
|
||||
"aboutBugCopyInfoInstruction": "시스템 정보를 복사하기",
|
||||
"aboutBugCopyInfoButton": "복사",
|
||||
"aboutBugReportInstruction": "로그와 시스템 정보를 첨부하여 깃허브에서 이슈를 제출하기",
|
||||
|
@ -417,6 +418,7 @@
|
|||
|
||||
"searchCollectionFieldHint": "미디어 검색",
|
||||
"searchSectionRecent": "최근 검색기록",
|
||||
"searchSectionDate": "날짜",
|
||||
"searchSectionAlbums": "앨범",
|
||||
"searchSectionCountries": "국가",
|
||||
"searchSectionPlaces": "장소",
|
||||
|
@ -502,6 +504,7 @@
|
|||
"settingsViewerSlideshowTitle": "슬라이드쇼",
|
||||
"settingsSlideshowRepeat": "반복",
|
||||
"settingsSlideshowShuffle": "순서섞기",
|
||||
"settingsSlideshowFillScreen": "화면 채우기",
|
||||
"settingsSlideshowTransitionTile": "전환 효과",
|
||||
"settingsSlideshowTransitionTitle": "전환 효과",
|
||||
"settingsSlideshowIntervalTile": "교체 주기",
|
||||
|
@ -584,6 +587,11 @@
|
|||
"settingsUnitSystemTile": "단위법",
|
||||
"settingsUnitSystemTitle": "단위법",
|
||||
|
||||
"settingsScreenSaverPageTitle": "화면보호기",
|
||||
|
||||
"settingsWidgetPageTitle": "사진 액자",
|
||||
"settingsWidgetShowOutline": "윤곽",
|
||||
|
||||
"statsPageTitle": "통계",
|
||||
"statsWithGps": "{count, plural, other{{count}개 위치가 있음}}",
|
||||
"statsTopCountries": "국가 랭킹",
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "Mostrar",
|
||||
"hideTooltip": "Ocultar",
|
||||
"actionRemove": "Remover",
|
||||
"resetButtonTooltip": "Resetar",
|
||||
"resetTooltip": "Resetar",
|
||||
"saveTooltip": "Salve",
|
||||
|
||||
"doubleBackExitMessage": "Toque em “voltar” novamente para sair.",
|
||||
"doNotAskAgain": "Não pergunte novamente",
|
||||
|
@ -94,6 +95,7 @@
|
|||
"filterFavouriteLabel": "Favorito",
|
||||
"filterLocationEmptyLabel": "Não localizado",
|
||||
"filterTagEmptyLabel": "Sem etiqueta",
|
||||
"filterOnThisDayLabel": "Neste dia",
|
||||
"filterRatingUnratedLabel": "Sem classificação",
|
||||
"filterRatingRejectedLabel": "Rejeitado",
|
||||
"filterTypeAnimatedLabel": "Animado",
|
||||
|
@ -303,7 +305,6 @@
|
|||
|
||||
"aboutBug": "Relatório de erro",
|
||||
"aboutBugSaveLogInstruction": "Salvar registros de aplicativos em um arquivo",
|
||||
"aboutBugSaveLogButton": "Salve",
|
||||
"aboutBugCopyInfoInstruction": "Copiar informações do sistema",
|
||||
"aboutBugCopyInfoButton": "Copiar",
|
||||
"aboutBugReportInstruction": "Relatório no GitHub com os logs e informações do sistema",
|
||||
|
@ -417,6 +418,7 @@
|
|||
|
||||
"searchCollectionFieldHint": "Pesquisar coleção",
|
||||
"searchSectionRecent": "Recente",
|
||||
"searchSectionDate": "Data",
|
||||
"searchSectionAlbums": "Álbuns",
|
||||
"searchSectionCountries": "Países",
|
||||
"searchSectionPlaces": "Locais",
|
||||
|
@ -502,6 +504,7 @@
|
|||
"settingsViewerSlideshowTitle": "Apresentação de slides",
|
||||
"settingsSlideshowRepeat": "Repetir",
|
||||
"settingsSlideshowShuffle": "Embaralhar",
|
||||
"settingsSlideshowFillScreen": "Preencher tela",
|
||||
"settingsSlideshowTransitionTile": "Transição",
|
||||
"settingsSlideshowTransitionTitle": "Transição",
|
||||
"settingsSlideshowIntervalTile": "Intervalo",
|
||||
|
@ -584,6 +587,11 @@
|
|||
"settingsUnitSystemTile": "Unidades",
|
||||
"settingsUnitSystemTitle": "Unidades",
|
||||
|
||||
"settingsScreenSaverPageTitle": "Protetor de tela",
|
||||
|
||||
"settingsWidgetPageTitle": "Porta-retratos",
|
||||
"settingsWidgetShowOutline": "Contorno",
|
||||
|
||||
"statsPageTitle": "Estatísticas",
|
||||
"statsWithGps": "{count, plural, =1{1 item com localização} other{{count} itens com localização}}",
|
||||
"statsTopCountries": "Principais Países",
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "Показать",
|
||||
"hideTooltip": "Скрыть",
|
||||
"actionRemove": "Удалить",
|
||||
"resetButtonTooltip": "Сбросить",
|
||||
"resetTooltip": "Сбросить",
|
||||
"saveTooltip": "Сохранить",
|
||||
|
||||
"doubleBackExitMessage": "Нажмите «Назад» еще раз, чтобы выйти.",
|
||||
"doNotAskAgain": "Больше не спрашивать",
|
||||
|
@ -81,6 +82,9 @@
|
|||
"videoActionSetSpeed": "Скорость вопспроизведения",
|
||||
"videoActionSettings": "Настройки",
|
||||
|
||||
"slideshowActionResume": "Продолжить",
|
||||
"slideshowActionShowInCollection": "Показать в Коллекции",
|
||||
|
||||
"entryInfoActionEditDate": "Изменить дату и время",
|
||||
"entryInfoActionEditLocation": "Изменить местоположение",
|
||||
"entryInfoActionEditRating": "Изменить рейтинг",
|
||||
|
@ -145,10 +149,23 @@
|
|||
"displayRefreshRatePreferHighest": "Наивысшая частота",
|
||||
"displayRefreshRatePreferLowest": "Наименьшая частота",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "Пропустить",
|
||||
"slideshowVideoPlaybackMuted": "Играть без звука",
|
||||
"slideshowVideoPlaybackWithSound": "Играть со звуком",
|
||||
|
||||
"themeBrightnessLight": "Светлая",
|
||||
"themeBrightnessDark": "Тёмная",
|
||||
"themeBrightnessBlack": "Чёрная",
|
||||
|
||||
"viewerTransitionSlide": "Скольжение",
|
||||
"viewerTransitionParallax": "Параллакс",
|
||||
"viewerTransitionFade": "Затухание",
|
||||
"viewerTransitionZoomIn": "Приближение",
|
||||
|
||||
"wallpaperTargetHome": "Домашний экран",
|
||||
"wallpaperTargetLock": "Экран блокировки",
|
||||
"wallpaperTargetHomeLock": "Домашний экран и экран блокировки",
|
||||
|
||||
"albumTierNew": "Новые",
|
||||
"albumTierPinned": "Закрепленные",
|
||||
"albumTierSpecial": "Стандартные",
|
||||
|
@ -263,6 +280,7 @@
|
|||
"menuActionSelectAll": "Выбрать все",
|
||||
"menuActionSelectNone": "Снять выделение",
|
||||
"menuActionMap": "Карта",
|
||||
"menuActionSlideshow": "Слайд-шоу",
|
||||
"menuActionStats": "Статистика",
|
||||
|
||||
"viewDialogTabSort": "Сортировка",
|
||||
|
@ -286,7 +304,6 @@
|
|||
|
||||
"aboutBug": "Отчет об ошибке",
|
||||
"aboutBugSaveLogInstruction": "Сохраните логи приложения в файл",
|
||||
"aboutBugSaveLogButton": "Сохранить",
|
||||
"aboutBugCopyInfoInstruction": "Скопируйте системную информацию",
|
||||
"aboutBugCopyInfoButton": "Скопировать",
|
||||
"aboutBugReportInstruction": "Отправьте отчёт об ошибке на GitHub вместе с логами и системной информацией",
|
||||
|
@ -350,6 +367,7 @@
|
|||
"collectionEmptyFavourites": "Нет избранных",
|
||||
"collectionEmptyVideos": "Нет видео",
|
||||
"collectionEmptyImages": "Нет изображений",
|
||||
"collectionEmptyGrantAccessButtonLabel": "Предоставить доступ",
|
||||
|
||||
"collectionSelectSectionTooltip": "Выбрать раздел",
|
||||
"collectionDeselectSectionTooltip": "Снять выбор с раздела",
|
||||
|
@ -399,6 +417,7 @@
|
|||
|
||||
"searchCollectionFieldHint": "Поиск по коллекции",
|
||||
"searchSectionRecent": "Недавние",
|
||||
"searchSectionDate": "Дата",
|
||||
"searchSectionAlbums": "Альбомы",
|
||||
"searchSectionCountries": "Страны",
|
||||
"searchSectionPlaces": "Локации",
|
||||
|
@ -480,6 +499,17 @@
|
|||
"settingsViewerShowOverlayThumbnails": "Показать эскизы",
|
||||
"settingsViewerEnableOverlayBlurEffect": "Наложение эффекта размытия",
|
||||
|
||||
"settingsViewerSlideshowTile": "Слайд-шоу",
|
||||
"settingsViewerSlideshowTitle": "Слайд-шоу",
|
||||
"settingsSlideshowRepeat": "Повтор",
|
||||
"settingsSlideshowShuffle": "Вперемешку",
|
||||
"settingsSlideshowTransitionTile": "Эффект перехода",
|
||||
"settingsSlideshowTransitionTitle": "Эффект Перехода",
|
||||
"settingsSlideshowIntervalTile": "Интервал",
|
||||
"settingsSlideshowIntervalTitle": "Интервал",
|
||||
"settingsSlideshowVideoPlaybackTile": "Проигрывание видео",
|
||||
"settingsSlideshowVideoPlaybackTitle": "Проигрывание Видео",
|
||||
|
||||
"settingsVideoPageTitle": "Настройки видео",
|
||||
"settingsSectionVideo": "Видео",
|
||||
"settingsVideoShowVideos": "Показать видео",
|
||||
|
@ -544,6 +574,7 @@
|
|||
"settingsSectionDisplay": "Отображение",
|
||||
"settingsThemeBrightness": "Тема",
|
||||
"settingsThemeColorHighlights": "Цветовые акценты",
|
||||
"settingsThemeEnableDynamicColor": "Динамический цвет",
|
||||
"settingsDisplayRefreshRateModeTile": "Частота обновления экрана",
|
||||
"settingsDisplayRefreshRateModeTitle": "Частота обновления",
|
||||
|
||||
|
@ -561,6 +592,7 @@
|
|||
"statsTopTags": "Топ тегов",
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "ОТКРЫТЬ ПАНОРАМУ",
|
||||
"viewerSetWallpaperButtonLabel": "УСТАНОВИТЬ КАК ОБОИ",
|
||||
"viewerErrorUnknown": "Упс!",
|
||||
"viewerErrorDoesNotExist": "Файл больше не существует.",
|
||||
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "Göster",
|
||||
"hideTooltip": "Gizle",
|
||||
"actionRemove": "Kaldır",
|
||||
"resetButtonTooltip": "Sıfırla",
|
||||
"resetTooltip": "Sıfırla",
|
||||
"saveTooltip": "Kaydet",
|
||||
|
||||
"doubleBackExitMessage": "Çıkmak için tekrar “geri”, düğmesine dokunun.",
|
||||
"doNotAskAgain": "Bir daha sorma",
|
||||
|
@ -295,7 +296,6 @@
|
|||
|
||||
"aboutBug": "Hata Bildirimi",
|
||||
"aboutBugSaveLogInstruction": "Uygulama günlüklerini bir dosyaya kaydet",
|
||||
"aboutBugSaveLogButton": "Kaydet",
|
||||
"aboutBugCopyInfoInstruction": "Sistem bilgilerini kopyala",
|
||||
"aboutBugCopyInfoButton": "Kopyala",
|
||||
"aboutBugReportInstruction": "GitHub'da günlükleri ve sistem bilgilerini içeren bir rapor oluştur",
|
||||
|
@ -418,6 +418,7 @@
|
|||
|
||||
"searchCollectionFieldHint": "Koleksiyonu ara",
|
||||
"searchSectionRecent": "Yakın zamanda",
|
||||
"searchSectionDate": "Tarih",
|
||||
"searchSectionAlbums": "Albümler",
|
||||
"searchSectionCountries": "Ülkeler",
|
||||
"searchSectionPlaces": "Yerler",
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "显示",
|
||||
"hideTooltip": "隐藏",
|
||||
"actionRemove": "移除",
|
||||
"resetButtonTooltip": "重置",
|
||||
"resetTooltip": "重置",
|
||||
"saveTooltip": "保存",
|
||||
|
||||
"doubleBackExitMessage": "再按一次退出",
|
||||
"doNotAskAgain": "不再询问",
|
||||
|
@ -94,6 +95,7 @@
|
|||
"filterFavouriteLabel": "收藏夹",
|
||||
"filterLocationEmptyLabel": "未定位",
|
||||
"filterTagEmptyLabel": "无标签",
|
||||
"filterOnThisDayLabel": "选择日期",
|
||||
"filterRatingUnratedLabel": "未评分",
|
||||
"filterRatingRejectedLabel": "拒绝",
|
||||
"filterTypeAnimatedLabel": "动画",
|
||||
|
@ -303,7 +305,6 @@
|
|||
|
||||
"aboutBug": "报告错误",
|
||||
"aboutBugSaveLogInstruction": "将应用日志保存到文件",
|
||||
"aboutBugSaveLogButton": "保存",
|
||||
"aboutBugCopyInfoInstruction": "复制系统信息",
|
||||
"aboutBugCopyInfoButton": "复制",
|
||||
"aboutBugReportInstruction": "在 GitHub 上报告日志和系统信息",
|
||||
|
@ -417,6 +418,7 @@
|
|||
|
||||
"searchCollectionFieldHint": "搜索媒体集",
|
||||
"searchSectionRecent": "最近",
|
||||
"searchSectionDate": "日期",
|
||||
"searchSectionAlbums": "相册",
|
||||
"searchSectionCountries": "国家",
|
||||
"searchSectionPlaces": "地点",
|
||||
|
@ -502,6 +504,7 @@
|
|||
"settingsViewerSlideshowTitle": "幻灯片",
|
||||
"settingsSlideshowRepeat": "重复",
|
||||
"settingsSlideshowShuffle": "随机播放",
|
||||
"settingsSlideshowFillScreen": "填充屏幕",
|
||||
"settingsSlideshowTransitionTile": "过渡动画",
|
||||
"settingsSlideshowTransitionTitle": "过渡动画",
|
||||
"settingsSlideshowIntervalTile": "时间间隔",
|
||||
|
@ -584,6 +587,11 @@
|
|||
"settingsUnitSystemTile": "单位",
|
||||
"settingsUnitSystemTitle": "单位",
|
||||
|
||||
"settingsScreenSaverPageTitle": "屏保",
|
||||
|
||||
"settingsWidgetPageTitle": "相框",
|
||||
"settingsWidgetShowOutline": "轮廓",
|
||||
|
||||
"statsPageTitle": "统计",
|
||||
"statsWithGps": "{count, plural, other{{count} 项带位置信息}}",
|
||||
"statsTopCountries": "热门国家",
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import 'package:aves/app_flavor.dart';
|
||||
import 'package:aves/main_common.dart';
|
||||
import 'package:aves/widget_common.dart';
|
||||
|
||||
void main() {
|
||||
mainCommon(AppFlavor.huawei);
|
||||
}
|
||||
const _flavor = AppFlavor.huawei;
|
||||
|
||||
void main() => mainCommon(_flavor);
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void widgetMain() => widgetMainCommon(_flavor);
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import 'package:aves/app_flavor.dart';
|
||||
import 'package:aves/main_common.dart';
|
||||
import 'package:aves/widget_common.dart';
|
||||
|
||||
void main() {
|
||||
mainCommon(AppFlavor.izzy);
|
||||
}
|
||||
const _flavor = AppFlavor.izzy;
|
||||
|
||||
void main() => mainCommon(_flavor);
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void widgetMain() => widgetMainCommon(_flavor);
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import 'package:aves/app_flavor.dart';
|
||||
import 'package:aves/main_common.dart';
|
||||
import 'package:aves/widget_common.dart';
|
||||
|
||||
void main() {
|
||||
mainCommon(AppFlavor.play);
|
||||
}
|
||||
const _flavor = AppFlavor.play;
|
||||
|
||||
void main() => mainCommon(_flavor);
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void widgetMain() => widgetMainCommon(_flavor);
|
||||
|
|
|
@ -249,7 +249,9 @@ class AvesEntry {
|
|||
|
||||
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);
|
||||
|
||||
|
@ -689,7 +691,7 @@ class AvesEntry {
|
|||
await metadataDb.removeIds({id}, dataTypes: dataTypes);
|
||||
}
|
||||
|
||||
final updatedEntry = await mediaFileService.getEntry(uri, mimeType);
|
||||
final updatedEntry = await mediaFetchService.getEntry(uri, mimeType);
|
||||
if (updatedEntry != null) {
|
||||
await applyNewFields(updatedEntry.toMap(), persist: persist);
|
||||
}
|
||||
|
@ -699,7 +701,7 @@ class AvesEntry {
|
|||
|
||||
Future<bool> delete() {
|
||||
final completer = Completer<bool>();
|
||||
mediaFileService.delete(entries: {this}).listen(
|
||||
mediaEditService.delete(entries: {this}).listen(
|
||||
(event) => completer.complete(event.success && !event.skipped),
|
||||
onError: completer.completeError,
|
||||
onDone: () {
|
||||
|
|
|
@ -58,9 +58,13 @@ class EntryDir {
|
|||
var resolved = vrl.volumePath;
|
||||
final parts = pContext.split(vrl.relativeDir);
|
||||
for (final part in parts) {
|
||||
final partLower = part.toLowerCase();
|
||||
final childrenDirs = Directory(resolved).listSync().where((v) => v.absolute is Directory).toSet();
|
||||
final found = childrenDirs.firstWhereOrNull((v) => pContext.basename(v.path).toLowerCase() == partLower);
|
||||
FileSystemEntity? found;
|
||||
final dir = Directory(resolved);
|
||||
if (dir.existsSync()) {
|
||||
final partLower = part.toLowerCase();
|
||||
final childrenDirs = dir.listSync().where((v) => v.absolute is Directory).toSet();
|
||||
found = childrenDirs.firstWhereOrNull((v) => pContext.basename(v.path).toLowerCase() == partLower);
|
||||
}
|
||||
resolved = found?.path ?? '$resolved${pContext.separator}$part';
|
||||
}
|
||||
return resolved;
|
||||
|
|
|
@ -20,11 +20,12 @@ class AlbumFilter extends CoveredCollectionFilter {
|
|||
|
||||
const AlbumFilter(this.album, this.displayName);
|
||||
|
||||
AlbumFilter.fromMap(Map<String, dynamic> json)
|
||||
: this(
|
||||
json['album'],
|
||||
json['uniqueName'],
|
||||
);
|
||||
factory AlbumFilter.fromMap(Map<String, dynamic> json) {
|
||||
return AlbumFilter(
|
||||
json['album'],
|
||||
json['uniqueName'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
|
|
|
@ -23,11 +23,12 @@ class CoordinateFilter extends CollectionFilter {
|
|||
|
||||
const CoordinateFilter(this.sw, this.ne, {this.minuteSecondPadding = false});
|
||||
|
||||
CoordinateFilter.fromMap(Map<String, dynamic> json)
|
||||
: this(
|
||||
LatLng.fromJson(json['sw']),
|
||||
LatLng.fromJson(json['ne']),
|
||||
);
|
||||
factory CoordinateFilter.fromMap(Map<String, dynamic> json) {
|
||||
return CoordinateFilter(
|
||||
LatLng.fromJson(json['sw']),
|
||||
LatLng.fromJson(json['ne']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
|
|
137
lib/model/filters/date.dart
Normal file
137
lib/model/filters/date.dart
Normal 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 }
|
|
@ -4,6 +4,7 @@ import 'package:aves/model/covers.dart';
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/album.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/location.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
|
@ -28,6 +29,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
MimeFilter.type,
|
||||
AlbumFilter.type,
|
||||
TypeFilter.type,
|
||||
DateFilter.type,
|
||||
LocationFilter.type,
|
||||
CoordinateFilter.type,
|
||||
FavouriteFilter.type,
|
||||
|
@ -52,6 +54,8 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
return AlbumFilter.fromMap(jsonMap);
|
||||
case CoordinateFilter.type:
|
||||
return CoordinateFilter.fromMap(jsonMap);
|
||||
case DateFilter.type:
|
||||
return DateFilter.fromMap(jsonMap);
|
||||
case FavouriteFilter.type:
|
||||
return FavouriteFilter.instance;
|
||||
case LocationFilter.type:
|
||||
|
@ -85,7 +89,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
|
||||
EntryFilter get test;
|
||||
|
||||
bool get isUnique => true;
|
||||
bool isCompatible(CollectionFilter other) => category != other.category;
|
||||
|
||||
String get universalLabel;
|
||||
|
||||
|
|
|
@ -31,11 +31,12 @@ class LocationFilter extends CoveredCollectionFilter {
|
|||
}
|
||||
}
|
||||
|
||||
LocationFilter.fromMap(Map<String, dynamic> json)
|
||||
: this(
|
||||
LocationLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? LocationLevel.place,
|
||||
json['location'],
|
||||
);
|
||||
factory LocationFilter.fromMap(Map<String, dynamic> json) {
|
||||
return LocationFilter(
|
||||
LocationLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? LocationLevel.place,
|
||||
json['location'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
|
|
|
@ -43,10 +43,11 @@ class MimeFilter extends CollectionFilter {
|
|||
_icon = icon ?? AIcons.vector;
|
||||
}
|
||||
|
||||
MimeFilter.fromMap(Map<String, dynamic> json)
|
||||
: this(
|
||||
json['mime'],
|
||||
);
|
||||
factory MimeFilter.fromMap(Map<String, dynamic> json) {
|
||||
return MimeFilter(
|
||||
json['mime'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
|
|
|
@ -15,10 +15,11 @@ class PathFilter extends CollectionFilter {
|
|||
|
||||
PathFilter(this.path) : _rootAlbum = path.substring(0, path.length - 1);
|
||||
|
||||
PathFilter.fromMap(Map<String, dynamic> json)
|
||||
: this(
|
||||
json['path'],
|
||||
);
|
||||
factory PathFilter.fromMap(Map<String, dynamic> json) {
|
||||
return PathFilter(
|
||||
json['path'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
|
|
|
@ -59,10 +59,11 @@ class QueryFilter extends CollectionFilter {
|
|||
_test = not ? (entry) => !testTitle(entry) : testTitle;
|
||||
}
|
||||
|
||||
QueryFilter.fromMap(Map<String, dynamic> json)
|
||||
: this(
|
||||
json['query'],
|
||||
);
|
||||
factory QueryFilter.fromMap(Map<String, dynamic> json) {
|
||||
return QueryFilter(
|
||||
json['query'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
|
@ -74,7 +75,7 @@ class QueryFilter extends CollectionFilter {
|
|||
EntryFilter get test => _test;
|
||||
|
||||
@override
|
||||
bool get isUnique => false;
|
||||
bool isCompatible(CollectionFilter other) => true;
|
||||
|
||||
@override
|
||||
String get universalLabel => query;
|
||||
|
|
|
@ -13,10 +13,11 @@ class RatingFilter extends CollectionFilter {
|
|||
|
||||
const RatingFilter(this.rating);
|
||||
|
||||
RatingFilter.fromMap(Map<String, dynamic> json)
|
||||
: this(
|
||||
json['rating'] ?? 0,
|
||||
);
|
||||
factory RatingFilter.fromMap(Map<String, dynamic> json) {
|
||||
return RatingFilter(
|
||||
json['rating'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
|
|
|
@ -20,11 +20,12 @@ class TagFilter extends CoveredCollectionFilter {
|
|||
}
|
||||
}
|
||||
|
||||
TagFilter.fromMap(Map<String, dynamic> json)
|
||||
: this(
|
||||
json['tag'],
|
||||
not: json['not'] ?? false,
|
||||
);
|
||||
factory TagFilter.fromMap(Map<String, dynamic> json) {
|
||||
return TagFilter(
|
||||
json['tag'],
|
||||
not: json['not'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
|
@ -37,7 +38,7 @@ class TagFilter extends CoveredCollectionFilter {
|
|||
EntryFilter get test => _test;
|
||||
|
||||
@override
|
||||
bool get isUnique => false;
|
||||
bool isCompatible(CollectionFilter other) => true;
|
||||
|
||||
@override
|
||||
String get universalLabel => tag;
|
||||
|
|
|
@ -59,10 +59,11 @@ class TypeFilter extends CollectionFilter {
|
|||
}
|
||||
}
|
||||
|
||||
TypeFilter.fromMap(Map<String, dynamic> json)
|
||||
: this._private(
|
||||
json['itemType'],
|
||||
);
|
||||
factory TypeFilter.fromMap(Map<String, dynamic> json) {
|
||||
return TypeFilter._private(
|
||||
json['itemType'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
|
|
|
@ -31,7 +31,7 @@ class SettingsDefaults {
|
|||
static const mustBackTwiceToExit = true;
|
||||
static const keepScreenOn = KeepScreenOn.viewerOnly;
|
||||
static const homePage = HomePageSetting.collection;
|
||||
static const showBottomNavigationBar = true;
|
||||
static const enableBottomNavigationBar = true;
|
||||
static const confirmDeleteForever = true;
|
||||
static const confirmMoveToBin = true;
|
||||
static const confirmMoveUndatedItems = true;
|
||||
|
@ -128,10 +128,15 @@ class SettingsDefaults {
|
|||
// slideshow
|
||||
static const slideshowRepeat = false;
|
||||
static const slideshowShuffle = false;
|
||||
static const slideshowFillScreen = false;
|
||||
static const slideshowTransition = ViewerTransition.fade;
|
||||
static const slideshowVideoPlayback = SlideshowVideoPlayback.playMuted;
|
||||
static const slideshowInterval = SlideshowInterval.s5;
|
||||
|
||||
// widget
|
||||
static const widgetOutline = false;
|
||||
static const widgetShape = WidgetShape.rrect;
|
||||
|
||||
// platform settings
|
||||
static const isRotationLocked = false;
|
||||
static const areAnimationsRemoved = false;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:aves/services/common/services.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_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');
|
||||
switch (this) {
|
||||
case DisplayRefreshRateMode.auto:
|
||||
FlutterDisplayMode.setPreferredMode(DisplayMode.auto);
|
||||
await FlutterDisplayMode.setPreferredMode(DisplayMode.auto);
|
||||
break;
|
||||
case DisplayRefreshRateMode.highest:
|
||||
FlutterDisplayMode.setHighRefreshRate();
|
||||
await FlutterDisplayMode.setHighRefreshRate();
|
||||
break;
|
||||
case DisplayRefreshRateMode.lowest:
|
||||
FlutterDisplayMode.setLowRefreshRate();
|
||||
await FlutterDisplayMode.setLowRefreshRate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,3 +29,5 @@ enum VideoControls { play, playSeek, playOutside, none }
|
|||
enum VideoLoopMode { never, shortOnly, always }
|
||||
|
||||
enum ViewerTransition { slide, parallax, fade, zoomIn }
|
||||
|
||||
enum WidgetShape { rrect, circle, heart }
|
44
lib/model/settings/enums/widget_shape.dart
Normal file
44
lib/model/settings/enums/widget_shape.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,7 +27,7 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
Settings._private();
|
||||
|
||||
static const Set<String> internalKeys = {
|
||||
static const Set<String> _internalKeys = {
|
||||
hasAcceptedTermsKey,
|
||||
catalogTimeZoneKey,
|
||||
videoShowRawTimedTextKey,
|
||||
|
@ -36,6 +36,7 @@ class Settings extends ChangeNotifier {
|
|||
platformTransitionAnimationScaleKey,
|
||||
topEntryIdsKey,
|
||||
};
|
||||
static const _widgetKeyPrefix = 'widget_';
|
||||
|
||||
// app
|
||||
static const hasAcceptedTermsKey = 'has_accepted_terms';
|
||||
|
@ -60,7 +61,7 @@ class Settings extends ChangeNotifier {
|
|||
static const mustBackTwiceToExitKey = 'must_back_twice_to_exit';
|
||||
static const keepScreenOnKey = 'keep_screen_on';
|
||||
static const homePageKey = 'home_page';
|
||||
static const showBottomNavigationBarKey = 'show_bottom_navigation_bar';
|
||||
static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar';
|
||||
static const confirmDeleteForeverKey = 'confirm_delete_forever';
|
||||
static const confirmMoveToBinKey = 'confirm_move_to_bin';
|
||||
static const confirmMoveUndatedItemsKey = 'confirm_move_undated_items';
|
||||
|
@ -138,13 +139,27 @@ class Settings extends ChangeNotifier {
|
|||
// file picker
|
||||
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
|
||||
static const slideshowRepeatKey = 'slideshow_loop';
|
||||
static const slideshowShuffleKey = 'slideshow_shuffle';
|
||||
static const slideshowFillScreenKey = 'slideshow_fill_screen';
|
||||
static const slideshowTransitionKey = 'slideshow_transition';
|
||||
static const slideshowVideoPlaybackKey = 'slideshow_video_playback';
|
||||
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
|
||||
// cf Android `Settings.System.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 {
|
||||
if (includeInternalKeys) {
|
||||
await settingsStore.clear();
|
||||
} 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 {
|
||||
// performance
|
||||
final performanceClass = await deviceService.getPerformanceClass();
|
||||
|
@ -315,9 +334,9 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set homePage(HomePageSetting newValue) => setAndNotify(homePageKey, newValue.toString());
|
||||
|
||||
bool get showBottomNavigationBar => getBoolOrDefault(showBottomNavigationBarKey, SettingsDefaults.showBottomNavigationBar);
|
||||
bool get enableBottomNavigationBar => getBoolOrDefault(enableBottomNavigationBarKey, SettingsDefaults.enableBottomNavigationBar);
|
||||
|
||||
set showBottomNavigationBar(bool newValue) => setAndNotify(showBottomNavigationBarKey, newValue);
|
||||
set enableBottomNavigationBar(bool newValue) => setAndNotify(enableBottomNavigationBarKey, newValue);
|
||||
|
||||
bool get confirmDeleteForever => getBoolOrDefault(confirmDeleteForeverKey, SettingsDefaults.confirmDeleteForever);
|
||||
|
||||
|
@ -583,6 +602,28 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
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
|
||||
|
||||
bool get slideshowRepeat => getBoolOrDefault(slideshowRepeatKey, SettingsDefaults.slideshowRepeat);
|
||||
|
@ -593,6 +634,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
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);
|
||||
|
||||
set slideshowTransition(ViewerTransition newValue) => setAndNotify(slideshowTransitionKey, newValue.toString());
|
||||
|
@ -605,6 +650,27 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
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
|
||||
|
||||
int? getInt(String key) => settingsStore.getInt(key);
|
||||
|
@ -687,7 +753,7 @@ class Settings extends ChangeNotifier {
|
|||
// import/export
|
||||
|
||||
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 {
|
||||
|
@ -735,7 +801,7 @@ class Settings extends ChangeNotifier {
|
|||
case isErrorReportingAllowedKey:
|
||||
case enableDynamicColorKey:
|
||||
case enableBlurEffectKey:
|
||||
case showBottomNavigationBarKey:
|
||||
case enableBottomNavigationBarKey:
|
||||
case mustBackTwiceToExitKey:
|
||||
case confirmDeleteForeverKey:
|
||||
case confirmMoveToBinKey:
|
||||
|
@ -763,8 +829,10 @@ class Settings extends ChangeNotifier {
|
|||
case subtitleShowOutlineKey:
|
||||
case saveSearchHistoryKey:
|
||||
case filePickerShowHiddenFilesKey:
|
||||
case screenSaverFillScreenKey:
|
||||
case slideshowRepeatKey:
|
||||
case slideshowShuffleKey:
|
||||
case slideshowFillScreenKey:
|
||||
if (newValue is bool) {
|
||||
settingsStore.setBool(key, newValue);
|
||||
} else {
|
||||
|
@ -792,6 +860,9 @@ class Settings extends ChangeNotifier {
|
|||
case unitSystemKey:
|
||||
case accessibilityAnimationsKey:
|
||||
case timeToTakeActionKey:
|
||||
case screenSaverTransitionKey:
|
||||
case screenSaverVideoPlaybackKey:
|
||||
case screenSaverIntervalKey:
|
||||
case slideshowTransitionKey:
|
||||
case slideshowVideoPlaybackKey:
|
||||
case slideshowIntervalKey:
|
||||
|
@ -809,6 +880,7 @@ class Settings extends ChangeNotifier {
|
|||
case collectionBrowsingQuickActionsKey:
|
||||
case collectionSelectionQuickActionsKey:
|
||||
case viewerQuickActionsKey:
|
||||
case screenSaverCollectionFiltersKey:
|
||||
if (newValue is List) {
|
||||
settingsStore.setStringList(key, newValue.cast<String>());
|
||||
} else {
|
||||
|
|
|
@ -3,6 +3,8 @@ abstract class SettingsStore {
|
|||
|
||||
Future<void> init();
|
||||
|
||||
Future<void> reload();
|
||||
|
||||
Future<bool> clear();
|
||||
|
||||
Future<bool> remove(String key);
|
||||
|
|
|
@ -17,6 +17,9 @@ class SharedPrefSettingsStore implements SettingsStore {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reload() => _prefs!.reload();
|
||||
|
||||
@override
|
||||
Future<bool> clear() => _prefs!.clear();
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue