#175 photo frame widget
This commit is contained in:
parent
83fd5c91c4
commit
9d3a4777fc
79 changed files with 1563 additions and 530 deletions
|
@ -1,3 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
Android Studio Chipmunk (2021.2.1) recommends:
|
||||
- removing "package" from AndroidManifest.xml
|
||||
|
@ -165,6 +167,28 @@ This change eventually prevents building the app with Flutter v3.0.2.
|
|||
</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"
|
||||
|
|
|
@ -25,7 +25,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,11 +37,11 @@ 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
|
||||
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
|
||||
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
|
||||
|
|
|
@ -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,201 @@
|
|||
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()
|
||||
|
||||
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,6 +2,7 @@ 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
|
||||
|
@ -33,7 +34,7 @@ open class MainActivity : FlutterActivity() {
|
|||
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?) {
|
||||
|
@ -56,7 +57,7 @@ open class MainActivity : FlutterActivity() {
|
|||
// )
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val messenger = flutterEngine!!.dartExecutor.binaryMessenger
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
|
||||
// dart -> platform -> dart
|
||||
// - need Context
|
||||
|
@ -68,12 +69,14 @@ open 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 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))
|
||||
// - need Activity
|
||||
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
|
||||
|
@ -211,7 +214,7 @@ open class MainActivity : FlutterActivity() {
|
|||
}
|
||||
|
||||
open fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
||||
when (intent?.action) {
|
||||
when (val action = intent?.action) {
|
||||
Intent.ACTION_MAIN -> {
|
||||
intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page ->
|
||||
val filters = extractFiltersFromIntent(intent)
|
||||
|
@ -253,10 +256,19 @@ open class MainActivity : FlutterActivity() {
|
|||
INTENT_ACTION_PICK_COLLECTION_FILTERS -> {
|
||||
val initialFilters = extractFiltersFromIntent(intent)
|
||||
return hashMapOf(
|
||||
INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK_COLLECTION_FILTERS,
|
||||
INTENT_DATA_KEY_FILTERS to initialFilters
|
||||
INTENT_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
|
||||
}
|
||||
|
@ -365,14 +377,6 @@ open class MainActivity : FlutterActivity() {
|
|||
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_ITEMS = "pick_items"
|
||||
const val INTENT_ACTION_PICK_COLLECTION_FILTERS = "pick_collection_filters"
|
||||
const val INTENT_ACTION_SCREEN_SAVER = "screen_saver"
|
||||
|
@ -380,10 +384,22 @@ open class MainActivity : FlutterActivity() {
|
|||
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 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>()
|
||||
|
|
|
@ -92,19 +92,18 @@ class ScreenSaverService : DreamService() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initChannels() {
|
||||
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, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
|
||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
||||
// - need ContextWrapper
|
||||
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
|
||||
MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this))
|
||||
// - need Service
|
||||
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ServiceWindowHandler(this))
|
||||
|
||||
|
|
|
@ -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 ->
|
||||
|
@ -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
|
||||
}
|
||||
}
|
|
@ -28,16 +28,16 @@ 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 ContextWrapper
|
||||
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
|
||||
MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this))
|
||||
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this))
|
||||
// - need Activity
|
||||
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
|
||||
|
|
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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.content.ContextWrapper
|
||||
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,12 +11,9 @@ 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
|
||||
|
@ -27,19 +23,17 @@ import kotlinx.coroutines.SupervisorJob
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MediaFileHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler {
|
||||
class MediaFetchHandler(private val context: Context) : MethodCallHandler {
|
||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val density = contextWrapper.resources.displayMetrics.density
|
||||
private val density = context.resources.displayMetrics.density
|
||||
|
||||
private val regionFetcher = RegionFetcher(contextWrapper)
|
||||
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()
|
||||
}
|
||||
|
@ -59,7 +53,7 @@ class MediaFileHandler(private val contextWrapper: ContextWrapper) : MethodCallH
|
|||
return
|
||||
}
|
||||
|
||||
provider.fetchSingle(contextWrapper, 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)
|
||||
})
|
||||
|
@ -83,7 +77,7 @@ class MediaFileHandler(private val contextWrapper: ContextWrapper) : MethodCallH
|
|||
|
||||
// convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter
|
||||
ThumbnailFetcher(
|
||||
contextWrapper,
|
||||
context,
|
||||
uri,
|
||||
mimeType,
|
||||
dateModifiedSecs,
|
||||
|
@ -116,14 +110,14 @@ class MediaFileHandler(private val contextWrapper: ContextWrapper) : MethodCallH
|
|||
|
||||
val regionRect = Rect(x, y, x + width, y + height)
|
||||
when (mimeType) {
|
||||
MimeTypes.SVG -> SvgRegionFetcher(contextWrapper).fetch(
|
||||
MimeTypes.SVG -> SvgRegionFetcher(context).fetch(
|
||||
uri = uri,
|
||||
regionRect = regionRect,
|
||||
imageWidth = imageWidth,
|
||||
imageHeight = imageHeight,
|
||||
result = result,
|
||||
)
|
||||
MimeTypes.TIFF -> TiffRegionFetcher(contextWrapper).fetch(
|
||||
MimeTypes.TIFF -> TiffRegionFetcher(context).fetch(
|
||||
uri = uri,
|
||||
page = pageId ?: 0,
|
||||
sampleSize = sampleSize,
|
||||
|
@ -143,53 +137,12 @@ class MediaFileHandler(private val contextWrapper: ContextWrapper) : MethodCallH
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
private fun clearSizedThumbnailDiskCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
Glide.get(contextWrapper).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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
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">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" />
|
|
@ -33,7 +33,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
|||
final mimeType = key.mimeType;
|
||||
final pageId = key.pageId;
|
||||
try {
|
||||
final bytes = await mediaFileService.getRegion(
|
||||
final bytes = await mediaFetchService.getRegion(
|
||||
uri,
|
||||
mimeType,
|
||||
key.rotationDegrees,
|
||||
|
@ -58,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
|
||||
|
|
|
@ -35,7 +35,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
|||
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,
|
||||
|
@ -59,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
|
||||
|
|
|
@ -49,7 +49,7 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
|
|||
assert(key == this);
|
||||
|
||||
try {
|
||||
final bytes = await mediaFileService.getImage(
|
||||
final bytes = await mediaFetchService.getImage(
|
||||
uri,
|
||||
mimeType,
|
||||
rotationDegrees,
|
||||
|
|
|
@ -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",
|
||||
|
@ -303,7 +304,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",
|
||||
|
|
|
@ -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",
|
||||
|
@ -434,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",
|
||||
|
@ -767,7 +767,10 @@
|
|||
"settingsUnitSystemTile": "Units",
|
||||
"settingsUnitSystemTitle": "Units",
|
||||
|
||||
"settingsScreenSaverTitle": "Screen Saver",
|
||||
"settingsScreenSaverPageTitle": "Screen Saver",
|
||||
|
||||
"settingsWidgetPageTitle": "Photo Frame",
|
||||
"settingsWidgetShowOutline": "Outline",
|
||||
|
||||
"statsPageTitle": "Stats",
|
||||
"statsWithGps": "{count, plural, =1{1 item with location} other{{count} items with location}}",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
@ -303,7 +304,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",
|
||||
|
|
|
@ -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",
|
||||
|
@ -303,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",
|
||||
|
|
|
@ -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",
|
||||
|
@ -303,7 +304,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",
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "表示する",
|
||||
"hideTooltip": "非表示にする",
|
||||
"actionRemove": "削除",
|
||||
"resetButtonTooltip": "リセット",
|
||||
"resetTooltip": "リセット",
|
||||
"saveTooltip": "保存",
|
||||
|
||||
"doubleBackExitMessage": "終了するには「戻る」をもう一度タップしてください。",
|
||||
"doNotAskAgain": "今後このメッセージを表示しない",
|
||||
|
@ -303,7 +304,6 @@
|
|||
|
||||
"aboutBug": "バグの報告",
|
||||
"aboutBugSaveLogInstruction": "アプリのログをファイルに保存",
|
||||
"aboutBugSaveLogButton": "保存",
|
||||
"aboutBugCopyInfoInstruction": "システム情報をコピー",
|
||||
"aboutBugCopyInfoButton": "コピー",
|
||||
"aboutBugReportInstruction": "ログとシステム情報とともに GitHub で報告",
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "보기",
|
||||
"hideTooltip": "숨기기",
|
||||
"actionRemove": "제거",
|
||||
"resetButtonTooltip": "복원",
|
||||
"resetTooltip": "복원",
|
||||
"saveTooltip": "저장",
|
||||
|
||||
"doubleBackExitMessage": "종료하려면 한번 더 누르세요.",
|
||||
"doNotAskAgain": "다시 묻지 않기",
|
||||
|
@ -303,7 +304,6 @@
|
|||
|
||||
"aboutBug": "버그 보고",
|
||||
"aboutBugSaveLogInstruction": "앱 로그를 파일에 저장하기",
|
||||
"aboutBugSaveLogButton": "저장",
|
||||
"aboutBugCopyInfoInstruction": "시스템 정보를 복사하기",
|
||||
"aboutBugCopyInfoButton": "복사",
|
||||
"aboutBugReportInstruction": "로그와 시스템 정보를 첨부하여 깃허브에서 이슈를 제출하기",
|
||||
|
|
|
@ -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",
|
||||
|
@ -303,7 +304,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",
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "Показать",
|
||||
"hideTooltip": "Скрыть",
|
||||
"actionRemove": "Удалить",
|
||||
"resetButtonTooltip": "Сбросить",
|
||||
"resetTooltip": "Сбросить",
|
||||
"saveTooltip": "Сохранить",
|
||||
|
||||
"doubleBackExitMessage": "Нажмите «Назад» еще раз, чтобы выйти.",
|
||||
"doNotAskAgain": "Больше не спрашивать",
|
||||
|
@ -303,7 +304,6 @@
|
|||
|
||||
"aboutBug": "Отчет об ошибке",
|
||||
"aboutBugSaveLogInstruction": "Сохраните логи приложения в файл",
|
||||
"aboutBugSaveLogButton": "Сохранить",
|
||||
"aboutBugCopyInfoInstruction": "Скопируйте системную информацию",
|
||||
"aboutBugCopyInfoButton": "Скопировать",
|
||||
"aboutBugReportInstruction": "Отправьте отчёт об ошибке на GitHub вместе с логами и системной информацией",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"showTooltip": "显示",
|
||||
"hideTooltip": "隐藏",
|
||||
"actionRemove": "移除",
|
||||
"resetButtonTooltip": "重置",
|
||||
"resetTooltip": "重置",
|
||||
"saveTooltip": "保存",
|
||||
|
||||
"doubleBackExitMessage": "再按一次退出",
|
||||
"doNotAskAgain": "不再询问",
|
||||
|
@ -303,7 +304,6 @@
|
|||
|
||||
"aboutBug": "报告错误",
|
||||
"aboutBugSaveLogInstruction": "将应用日志保存到文件",
|
||||
"aboutBugSaveLogButton": "保存",
|
||||
"aboutBugCopyInfoInstruction": "复制系统信息",
|
||||
"aboutBugCopyInfoButton": "复制",
|
||||
"aboutBugReportInstruction": "在 GitHub 上报告日志和系统信息",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -691,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);
|
||||
}
|
||||
|
@ -701,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: () {
|
||||
|
|
|
@ -133,6 +133,10 @@ class SettingsDefaults {
|
|||
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;
|
||||
|
|
|
@ -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';
|
||||
|
@ -153,6 +154,12 @@ class Settings extends ChangeNotifier {
|
|||
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';
|
||||
|
@ -169,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();
|
||||
|
@ -639,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);
|
||||
|
@ -721,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 {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -235,7 +235,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
for (final kv in uriByContentId.entries) {
|
||||
final contentId = kv.key;
|
||||
final uri = kv.value;
|
||||
final sourceEntry = await mediaFileService.getEntry(uri, null);
|
||||
final sourceEntry = await mediaFetchService.getEntry(uri, null);
|
||||
if (sourceEntry != null) {
|
||||
final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId);
|
||||
// compare paths because some apps move files without updating their `last modified date`
|
||||
|
|
|
@ -19,7 +19,7 @@ mixin TrashMixin on SourceBase {
|
|||
|
||||
final processed = <ImageOpEvent>{};
|
||||
final completer = Completer<Set<String>>();
|
||||
mediaFileService.delete(entries: expiredEntries).listen(
|
||||
mediaEditService.delete(entries: expiredEntries).listen(
|
||||
processed.add,
|
||||
onError: completer.completeError,
|
||||
onDone: () async {
|
||||
|
|
|
@ -196,7 +196,7 @@ class PlatformAndroidAppService implements AndroidAppService {
|
|||
Uint8List? iconBytes;
|
||||
if (coverEntry != null) {
|
||||
final size = coverEntry.isVideo ? 0.0 : 256.0;
|
||||
iconBytes = await mediaFileService.getThumbnail(
|
||||
iconBytes = await mediaFetchService.getThumbnail(
|
||||
uri: coverEntry.uri,
|
||||
mimeType: coverEntry.mimeType,
|
||||
pageId: coverEntry.pageId,
|
||||
|
|
|
@ -6,7 +6,8 @@ import 'package:aves/model/settings/store/store_shared_pref.dart';
|
|||
import 'package:aves/services/android_app_service.dart';
|
||||
import 'package:aves/services/device_service.dart';
|
||||
import 'package:aves/services/media/embedded_data_service.dart';
|
||||
import 'package:aves/services/media/media_file_service.dart';
|
||||
import 'package:aves/services/media/media_edit_service.dart';
|
||||
import 'package:aves/services/media/media_fetch_service.dart';
|
||||
import 'package:aves/services/media/media_store_service.dart';
|
||||
import 'package:aves/services/metadata/metadata_edit_service.dart';
|
||||
import 'package:aves/services/metadata/metadata_fetch_service.dart';
|
||||
|
@ -31,7 +32,8 @@ final MetadataDb metadataDb = getIt<MetadataDb>();
|
|||
final AndroidAppService androidAppService = getIt<AndroidAppService>();
|
||||
final DeviceService deviceService = getIt<DeviceService>();
|
||||
final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>();
|
||||
final MediaFileService mediaFileService = getIt<MediaFileService>();
|
||||
final MediaEditService mediaEditService = getIt<MediaEditService>();
|
||||
final MediaFetchService mediaFetchService = getIt<MediaFetchService>();
|
||||
final MediaStoreService mediaStoreService = getIt<MediaStoreService>();
|
||||
final MetadataEditService metadataEditService = getIt<MetadataEditService>();
|
||||
final MetadataFetchService metadataFetchService = getIt<MetadataFetchService>();
|
||||
|
@ -48,7 +50,8 @@ void initPlatformServices() {
|
|||
getIt.registerLazySingleton<AndroidAppService>(PlatformAndroidAppService.new);
|
||||
getIt.registerLazySingleton<DeviceService>(PlatformDeviceService.new);
|
||||
getIt.registerLazySingleton<EmbeddedDataService>(PlatformEmbeddedDataService.new);
|
||||
getIt.registerLazySingleton<MediaFileService>(PlatformMediaFileService.new);
|
||||
getIt.registerLazySingleton<MediaEditService>(PlatformMediaEditService.new);
|
||||
getIt.registerLazySingleton<MediaFetchService>(PlatformMediaFetchService.new);
|
||||
getIt.registerLazySingleton<MediaStoreService>(PlatformMediaStoreService.new);
|
||||
getIt.registerLazySingleton<MetadataEditService>(PlatformMetadataEditService.new);
|
||||
getIt.registerLazySingleton<MetadataFetchService>(PlatformMetadataFetchService.new);
|
||||
|
|
216
lib/services/media/media_edit_service.dart
Normal file
216
lib/services/media/media_edit_service.dart
Normal file
|
@ -0,0 +1,216 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/services/common/image_op_events.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/services/media/enums.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:streams_channel/streams_channel.dart';
|
||||
|
||||
abstract class MediaEditService {
|
||||
String get newOpId;
|
||||
|
||||
Future<void> cancelFileOp(String opId);
|
||||
|
||||
Stream<ImageOpEvent> delete({
|
||||
String? opId,
|
||||
required Iterable<AvesEntry> entries,
|
||||
});
|
||||
|
||||
Stream<MoveOpEvent> move({
|
||||
String? opId,
|
||||
required Map<String, Iterable<AvesEntry>> entriesByDestination,
|
||||
required bool copy,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
});
|
||||
|
||||
Stream<ExportOpEvent> export(
|
||||
Iterable<AvesEntry> entries, {
|
||||
required EntryExportOptions options,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
});
|
||||
|
||||
Stream<MoveOpEvent> rename({
|
||||
String? opId,
|
||||
required Map<AvesEntry, String> entriesToNewName,
|
||||
});
|
||||
|
||||
Future<Map<String, dynamic>> captureFrame(
|
||||
AvesEntry entry, {
|
||||
required String desiredName,
|
||||
required Map<String, dynamic> exif,
|
||||
required Uint8List bytes,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
});
|
||||
}
|
||||
|
||||
class PlatformMediaEditService implements MediaEditService {
|
||||
static const _platform = MethodChannel('deckers.thibault/aves/media_edit');
|
||||
static final _opStream = StreamsChannel('deckers.thibault/aves/media_op_stream');
|
||||
|
||||
static Map<String, dynamic> _toPlatformEntryMap(AvesEntry entry) {
|
||||
return {
|
||||
'uri': entry.uri,
|
||||
'path': entry.path,
|
||||
'pageId': entry.pageId,
|
||||
'mimeType': entry.mimeType,
|
||||
'width': entry.width,
|
||||
'height': entry.height,
|
||||
'rotationDegrees': entry.rotationDegrees,
|
||||
'isFlipped': entry.isFlipped,
|
||||
'dateModifiedSecs': entry.dateModifiedSecs,
|
||||
'sizeBytes': entry.sizeBytes,
|
||||
'trashed': entry.trashed,
|
||||
'trashPath': entry.trashDetails?.path,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String get newOpId => DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
@override
|
||||
Future<void> cancelFileOp(String opId) async {
|
||||
try {
|
||||
await _platform.invokeMethod('cancelFileOp', <String, dynamic>{
|
||||
'opId': opId,
|
||||
});
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<ImageOpEvent> delete({
|
||||
String? opId,
|
||||
required Iterable<AvesEntry> entries,
|
||||
}) {
|
||||
try {
|
||||
return _opStream
|
||||
.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'delete',
|
||||
'id': opId,
|
||||
'entries': entries.map(_toPlatformEntryMap).toList(),
|
||||
})
|
||||
.where((event) => event is Map)
|
||||
.map((event) => ImageOpEvent.fromMap(event as Map));
|
||||
} on PlatformException catch (e, stack) {
|
||||
reportService.recordError(e, stack);
|
||||
return Stream.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<MoveOpEvent> move({
|
||||
String? opId,
|
||||
required Map<String, Iterable<AvesEntry>> entriesByDestination,
|
||||
required bool copy,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
}) {
|
||||
try {
|
||||
return _opStream
|
||||
.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'move',
|
||||
'id': opId,
|
||||
'entriesByDestination': entriesByDestination.map((destination, entries) => MapEntry(destination, entries.map(_toPlatformEntryMap).toList())),
|
||||
'copy': copy,
|
||||
'nameConflictStrategy': nameConflictStrategy.toPlatform(),
|
||||
})
|
||||
.where((event) => event is Map)
|
||||
.map((event) => MoveOpEvent.fromMap(event as Map));
|
||||
} on PlatformException catch (e, stack) {
|
||||
reportService.recordError(e, stack);
|
||||
return Stream.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<ExportOpEvent> export(
|
||||
Iterable<AvesEntry> entries, {
|
||||
required EntryExportOptions options,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
}) {
|
||||
try {
|
||||
return _opStream
|
||||
.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'export',
|
||||
'entries': entries.map(_toPlatformEntryMap).toList(),
|
||||
'mimeType': options.mimeType,
|
||||
'width': options.width,
|
||||
'height': options.height,
|
||||
'destinationPath': destinationAlbum,
|
||||
'nameConflictStrategy': nameConflictStrategy.toPlatform(),
|
||||
})
|
||||
.where((event) => event is Map)
|
||||
.map((event) => ExportOpEvent.fromMap(event as Map));
|
||||
} on PlatformException catch (e, stack) {
|
||||
reportService.recordError(e, stack);
|
||||
return Stream.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<MoveOpEvent> rename({
|
||||
String? opId,
|
||||
required Map<AvesEntry, String> entriesToNewName,
|
||||
}) {
|
||||
try {
|
||||
return _opStream
|
||||
.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'rename',
|
||||
'id': opId,
|
||||
'entriesToNewName': entriesToNewName.map((key, value) => MapEntry(_toPlatformEntryMap(key), value)),
|
||||
})
|
||||
.where((event) => event is Map)
|
||||
.map((event) => MoveOpEvent.fromMap(event as Map));
|
||||
} on PlatformException catch (e, stack) {
|
||||
reportService.recordError(e, stack);
|
||||
return Stream.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> captureFrame(
|
||||
AvesEntry entry, {
|
||||
required String desiredName,
|
||||
required Map<String, dynamic> exif,
|
||||
required Uint8List bytes,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _platform.invokeMethod('captureFrame', <String, dynamic>{
|
||||
'uri': entry.uri,
|
||||
'desiredName': desiredName,
|
||||
'exif': exif,
|
||||
'bytes': bytes,
|
||||
'destinationPath': destinationAlbum,
|
||||
'nameConflictStrategy': nameConflictStrategy.toPlatform(),
|
||||
});
|
||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class EntryExportOptions extends Equatable {
|
||||
final String mimeType;
|
||||
final int width, height;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [mimeType, width, height];
|
||||
|
||||
const EntryExportOptions({
|
||||
required this.mimeType,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
}
|
|
@ -5,19 +5,14 @@ import 'dart:ui';
|
|||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/services/common/image_op_events.dart';
|
||||
import 'package:aves/services/common/output_buffer.dart';
|
||||
import 'package:aves/services/common/service_policy.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/services/media/enums.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:streams_channel/streams_channel.dart';
|
||||
|
||||
abstract class MediaFileService {
|
||||
String get newOpId;
|
||||
|
||||
abstract class MediaFetchService {
|
||||
Future<AvesEntry?> getEntry(String uri, String? mimeType);
|
||||
|
||||
Future<Uint8List> getSvg(
|
||||
|
@ -70,69 +65,13 @@ abstract class MediaFileService {
|
|||
bool cancelThumbnail(Object taskKey);
|
||||
|
||||
Future<T>? resumeLoading<T>(Object taskKey);
|
||||
|
||||
Future<void> cancelFileOp(String opId);
|
||||
|
||||
Stream<ImageOpEvent> delete({
|
||||
String? opId,
|
||||
required Iterable<AvesEntry> entries,
|
||||
});
|
||||
|
||||
Stream<MoveOpEvent> move({
|
||||
String? opId,
|
||||
required Map<String, Iterable<AvesEntry>> entriesByDestination,
|
||||
required bool copy,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
});
|
||||
|
||||
Stream<ExportOpEvent> export(
|
||||
Iterable<AvesEntry> entries, {
|
||||
required EntryExportOptions options,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
});
|
||||
|
||||
Stream<MoveOpEvent> rename({
|
||||
String? opId,
|
||||
required Map<AvesEntry, String> entriesToNewName,
|
||||
});
|
||||
|
||||
Future<Map<String, dynamic>> captureFrame(
|
||||
AvesEntry entry, {
|
||||
required String desiredName,
|
||||
required Map<String, dynamic> exif,
|
||||
required Uint8List bytes,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
});
|
||||
}
|
||||
|
||||
class PlatformMediaFileService implements MediaFileService {
|
||||
static const _platform = MethodChannel('deckers.thibault/aves/media_file');
|
||||
class PlatformMediaFetchService implements MediaFetchService {
|
||||
static const _platform = MethodChannel('deckers.thibault/aves/media_fetch');
|
||||
static final _byteStream = StreamsChannel('deckers.thibault/aves/media_byte_stream');
|
||||
static final _opStream = StreamsChannel('deckers.thibault/aves/media_op_stream');
|
||||
static const double _thumbnailDefaultSize = 64.0;
|
||||
|
||||
static Map<String, dynamic> _toPlatformEntryMap(AvesEntry entry) {
|
||||
return {
|
||||
'uri': entry.uri,
|
||||
'path': entry.path,
|
||||
'pageId': entry.pageId,
|
||||
'mimeType': entry.mimeType,
|
||||
'width': entry.width,
|
||||
'height': entry.height,
|
||||
'rotationDegrees': entry.rotationDegrees,
|
||||
'isFlipped': entry.isFlipped,
|
||||
'dateModifiedSecs': entry.dateModifiedSecs,
|
||||
'sizeBytes': entry.sizeBytes,
|
||||
'trashed': entry.trashed,
|
||||
'trashPath': entry.trashDetails?.path,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String get newOpId => DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
@override
|
||||
Future<AvesEntry?> getEntry(String uri, String? mimeType) async {
|
||||
try {
|
||||
|
@ -315,145 +254,4 @@ class PlatformMediaFileService implements MediaFileService {
|
|||
|
||||
@override
|
||||
Future<T>? resumeLoading<T>(Object taskKey) => servicePolicy.resume<T>(taskKey);
|
||||
|
||||
@override
|
||||
Future<void> cancelFileOp(String opId) async {
|
||||
try {
|
||||
await _platform.invokeMethod('cancelFileOp', <String, dynamic>{
|
||||
'opId': opId,
|
||||
});
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<ImageOpEvent> delete({
|
||||
String? opId,
|
||||
required Iterable<AvesEntry> entries,
|
||||
}) {
|
||||
try {
|
||||
return _opStream
|
||||
.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'delete',
|
||||
'id': opId,
|
||||
'entries': entries.map(_toPlatformEntryMap).toList(),
|
||||
})
|
||||
.where((event) => event is Map)
|
||||
.map((event) => ImageOpEvent.fromMap(event as Map));
|
||||
} on PlatformException catch (e, stack) {
|
||||
reportService.recordError(e, stack);
|
||||
return Stream.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<MoveOpEvent> move({
|
||||
String? opId,
|
||||
required Map<String, Iterable<AvesEntry>> entriesByDestination,
|
||||
required bool copy,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
}) {
|
||||
try {
|
||||
return _opStream
|
||||
.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'move',
|
||||
'id': opId,
|
||||
'entriesByDestination': entriesByDestination.map((destination, entries) => MapEntry(destination, entries.map(_toPlatformEntryMap).toList())),
|
||||
'copy': copy,
|
||||
'nameConflictStrategy': nameConflictStrategy.toPlatform(),
|
||||
})
|
||||
.where((event) => event is Map)
|
||||
.map((event) => MoveOpEvent.fromMap(event as Map));
|
||||
} on PlatformException catch (e, stack) {
|
||||
reportService.recordError(e, stack);
|
||||
return Stream.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<ExportOpEvent> export(
|
||||
Iterable<AvesEntry> entries, {
|
||||
required EntryExportOptions options,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
}) {
|
||||
try {
|
||||
return _opStream
|
||||
.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'export',
|
||||
'entries': entries.map(_toPlatformEntryMap).toList(),
|
||||
'mimeType': options.mimeType,
|
||||
'width': options.width,
|
||||
'height': options.height,
|
||||
'destinationPath': destinationAlbum,
|
||||
'nameConflictStrategy': nameConflictStrategy.toPlatform(),
|
||||
})
|
||||
.where((event) => event is Map)
|
||||
.map((event) => ExportOpEvent.fromMap(event as Map));
|
||||
} on PlatformException catch (e, stack) {
|
||||
reportService.recordError(e, stack);
|
||||
return Stream.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<MoveOpEvent> rename({
|
||||
String? opId,
|
||||
required Map<AvesEntry, String> entriesToNewName,
|
||||
}) {
|
||||
try {
|
||||
return _opStream
|
||||
.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'rename',
|
||||
'id': opId,
|
||||
'entriesToNewName': entriesToNewName.map((key, value) => MapEntry(_toPlatformEntryMap(key), value)),
|
||||
})
|
||||
.where((event) => event is Map)
|
||||
.map((event) => MoveOpEvent.fromMap(event as Map));
|
||||
} on PlatformException catch (e, stack) {
|
||||
reportService.recordError(e, stack);
|
||||
return Stream.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> captureFrame(
|
||||
AvesEntry entry, {
|
||||
required String desiredName,
|
||||
required Map<String, dynamic> exif,
|
||||
required Uint8List bytes,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _platform.invokeMethod('captureFrame', <String, dynamic>{
|
||||
'uri': entry.uri,
|
||||
'desiredName': desiredName,
|
||||
'exif': exif,
|
||||
'bytes': bytes,
|
||||
'destinationPath': destinationAlbum,
|
||||
'nameConflictStrategy': nameConflictStrategy.toPlatform(),
|
||||
});
|
||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class EntryExportOptions extends Equatable {
|
||||
final String mimeType;
|
||||
final int width, height;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [mimeType, width, height];
|
||||
|
||||
const EntryExportOptions({
|
||||
required this.mimeType,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
}
|
|
@ -17,7 +17,7 @@ class SvgMetadataService {
|
|||
|
||||
static Future<Size?> getSize(AvesEntry entry) async {
|
||||
try {
|
||||
final data = await mediaFileService.getSvg(entry.uri, entry.mimeType);
|
||||
final data = await mediaFetchService.getSvg(entry.uri, entry.mimeType);
|
||||
|
||||
final document = XmlDocument.parse(utf8.decode(data));
|
||||
final root = document.rootElement;
|
||||
|
@ -63,7 +63,7 @@ class SvgMetadataService {
|
|||
}
|
||||
|
||||
try {
|
||||
final data = await mediaFileService.getSvg(entry.uri, entry.mimeType);
|
||||
final data = await mediaFetchService.getSvg(entry.uri, entry.mimeType);
|
||||
|
||||
final document = XmlDocument.parse(utf8.decode(data));
|
||||
final root = document.rootElement;
|
||||
|
|
29
lib/services/widget_service.dart
Normal file
29
lib/services/widget_service.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class WidgetService {
|
||||
static const _configureChannel = MethodChannel('deckers.thibault/aves/widget_configure');
|
||||
static const _updateChannel = MethodChannel('deckers.thibault/aves/widget_update');
|
||||
|
||||
static Future<bool> configure() async {
|
||||
try {
|
||||
await _configureChannel.invokeMethod('configure');
|
||||
return true;
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static Future<bool> update(int widgetId) async {
|
||||
try {
|
||||
await _updateChannel.invokeMethod('update', <String, dynamic>{
|
||||
'widgetId': widgetId,
|
||||
});
|
||||
return true;
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -9,6 +9,8 @@ class Constants {
|
|||
// so we give it a `strutStyle` with a slightly larger height
|
||||
static const overflowStrutStyle = StrutStyle(height: 1.3);
|
||||
|
||||
static const double colorPickerRadius = 16;
|
||||
|
||||
static const titleTextStyle = TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w300,
|
||||
|
@ -22,6 +24,11 @@ class Constants {
|
|||
)
|
||||
];
|
||||
|
||||
static const boraBoraGradientColors = [
|
||||
Color(0xff2bc0e4),
|
||||
Color(0xffeaecc6),
|
||||
];
|
||||
|
||||
// Bidi fun, cf https://www.unicode.org/reports/tr9/
|
||||
// First Strong Isolate
|
||||
static const fsi = '\u2068';
|
||||
|
|
82
lib/widget_common.dart
Normal file
82
lib/widget_common.dart
Normal file
|
@ -0,0 +1,82 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:aves/app_flavor.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/model/source/media_store_source.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/widgets/home_widget.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
const _widgetDrawChannel = MethodChannel('deckers.thibault/aves/widget_draw');
|
||||
|
||||
void widgetMainCommon(AppFlavor flavor) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
initPlatformServices();
|
||||
await settings.init(monitorPlatformSettings: false);
|
||||
|
||||
_widgetDrawChannel.setMethodCallHandler((call) async {
|
||||
// widget settings may be modified in a different process after channel setup
|
||||
await settings.reload();
|
||||
|
||||
switch (call.method) {
|
||||
case 'drawWidget':
|
||||
return _drawWidget(call.arguments);
|
||||
default:
|
||||
throw PlatformException(code: 'not-implemented', message: 'failed to handle method=${call.method}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<Uint8List> _drawWidget(dynamic args) async {
|
||||
final widgetId = args['widgetId'] as int;
|
||||
final widthPx = args['widthPx'] as int;
|
||||
final heightPx = args['heightPx'] as int;
|
||||
final devicePixelRatio = args['devicePixelRatio'] as double;
|
||||
final drawEntryImage = args['drawEntryImage'] as bool;
|
||||
final reuseEntry = args['reuseEntry'] as bool;
|
||||
|
||||
final entry = drawEntryImage ? await _getWidgetEntry(widgetId, reuseEntry) : null;
|
||||
final painter = HomeWidgetPainter(
|
||||
entry: entry,
|
||||
devicePixelRatio: devicePixelRatio,
|
||||
);
|
||||
return painter.drawWidget(
|
||||
widthPx: widthPx,
|
||||
heightPx: heightPx,
|
||||
outline: settings.getWidgetOutline(widgetId),
|
||||
shape: settings.getWidgetShape(widgetId),
|
||||
);
|
||||
}
|
||||
|
||||
Future<AvesEntry?> _getWidgetEntry(int widgetId, bool reuseEntry) async {
|
||||
final uri = reuseEntry ? settings.getWidgetUri(widgetId) : null;
|
||||
if (uri != null) {
|
||||
final entry = await mediaFetchService.getEntry(uri, null);
|
||||
if (entry != null) return entry;
|
||||
}
|
||||
|
||||
final filters = settings.getWidgetCollectionFilters(widgetId);
|
||||
final source = MediaStoreSource();
|
||||
final readyCompleter = Completer();
|
||||
source.stateNotifier.addListener(() {
|
||||
if (source.stateNotifier.value == SourceState.ready) {
|
||||
readyCompleter.complete();
|
||||
}
|
||||
});
|
||||
await source.init(canAnalyze: false);
|
||||
await readyCompleter.future;
|
||||
|
||||
final entries = CollectionLens(source: source, filters: filters).sortedEntries;
|
||||
entries.shuffle();
|
||||
final entry = entries.firstOrNull;
|
||||
if (entry != null) {
|
||||
settings.setWidgetUri(widgetId, entry.uri);
|
||||
}
|
||||
return entry;
|
||||
}
|
|
@ -68,7 +68,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildStep(1, l10n.aboutBugSaveLogInstruction, l10n.aboutBugSaveLogButton, _saveLogs),
|
||||
_buildStep(1, l10n.aboutBugSaveLogInstruction, l10n.saveTooltip, _saveLogs),
|
||||
_buildStep(2, l10n.aboutBugCopyInfoInstruction, l10n.aboutBugCopyInfoButton, _copySystemInfo),
|
||||
FutureBuilder<String>(
|
||||
future: _infoLoader,
|
||||
|
|
|
@ -12,12 +12,12 @@ import 'package:aves/model/source/collection_lens.dart';
|
|||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/services/intent_service.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/collection/collection_grid.dart';
|
||||
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
||||
import 'package:aves/widgets/common/basic/insets.dart';
|
||||
import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_fab.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/providers/query_provider.dart';
|
||||
import 'package:aves/widgets/common/providers/selection_provider.dart';
|
||||
|
@ -148,30 +148,11 @@ class _CollectionPageState extends State<CollectionPage> {
|
|||
}
|
||||
|
||||
Widget? _buildFab(BuildContext context, bool hasSelection) {
|
||||
Widget fab({
|
||||
required String tooltip,
|
||||
required VoidCallback onPressed,
|
||||
}) {
|
||||
return TooltipTheme(
|
||||
data: TooltipTheme.of(context).copyWith(
|
||||
preferBelow: false,
|
||||
),
|
||||
child: FloatingActionButton(
|
||||
tooltip: tooltip,
|
||||
onPressed: onPressed,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
child: const Icon(AIcons.apply),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||
switch (appMode) {
|
||||
case AppMode.pickMultipleMediaExternal:
|
||||
return hasSelection
|
||||
? fab(
|
||||
? AvesFab(
|
||||
tooltip: context.l10n.collectionPickPageTitle,
|
||||
onPressed: () {
|
||||
final items = context.read<Selection<AvesEntry>>().selectedItems;
|
||||
|
@ -181,7 +162,7 @@ class _CollectionPageState extends State<CollectionPage> {
|
|||
)
|
||||
: null;
|
||||
case AppMode.pickCollectionFiltersExternal:
|
||||
return fab(
|
||||
return AvesFab(
|
||||
tooltip: context.l10n.collectionPickPageTitle,
|
||||
onPressed: () {
|
||||
final filters = _collection.filters;
|
||||
|
|
|
@ -283,12 +283,12 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
if (!pureTrash && !await checkStoragePermissionForAlbums(context, selectionDirs, entries: entries)) return;
|
||||
|
||||
source.pauseMonitoring();
|
||||
final opId = mediaFileService.newOpId;
|
||||
final opId = mediaEditService.newOpId;
|
||||
await showOpReport<ImageOpEvent>(
|
||||
context: context,
|
||||
opStream: mediaFileService.delete(opId: opId, entries: entries),
|
||||
opStream: mediaEditService.delete(opId: opId, entries: entries),
|
||||
itemCount: todoCount,
|
||||
onCancel: () => mediaFileService.cancelFileOp(opId),
|
||||
onCancel: () => mediaEditService.cancelFileOp(opId),
|
||||
onDone: (processed) async {
|
||||
final successOps = processed.where((e) => e.success).toSet();
|
||||
final deletedOps = successOps.where((e) => !e.skipped).toSet();
|
||||
|
|
|
@ -119,17 +119,17 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
|||
|
||||
final source = context.read<CollectionSource>();
|
||||
source.pauseMonitoring();
|
||||
final opId = mediaFileService.newOpId;
|
||||
final opId = mediaEditService.newOpId;
|
||||
await showOpReport<MoveOpEvent>(
|
||||
context: context,
|
||||
opStream: mediaFileService.move(
|
||||
opStream: mediaEditService.move(
|
||||
opId: opId,
|
||||
entriesByDestination: entriesByDestination,
|
||||
copy: copy,
|
||||
nameConflictStrategy: nameConflictStrategy,
|
||||
),
|
||||
itemCount: todoCount,
|
||||
onCancel: () => mediaFileService.cancelFileOp(opId),
|
||||
onCancel: () => mediaEditService.cancelFileOp(opId),
|
||||
onDone: (processed) async {
|
||||
final successOps = processed.where((v) => v.success).toSet();
|
||||
|
||||
|
@ -226,15 +226,15 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
|||
|
||||
final source = context.read<CollectionSource>();
|
||||
source.pauseMonitoring();
|
||||
final opId = mediaFileService.newOpId;
|
||||
final opId = mediaEditService.newOpId;
|
||||
await showOpReport<MoveOpEvent>(
|
||||
context: context,
|
||||
opStream: mediaFileService.rename(
|
||||
opStream: mediaEditService.rename(
|
||||
opId: opId,
|
||||
entriesToNewName: entriesToNewName,
|
||||
),
|
||||
itemCount: todoCount,
|
||||
onCancel: () => mediaFileService.cancelFileOp(opId),
|
||||
onCancel: () => mediaEditService.cancelFileOp(opId),
|
||||
onDone: (processed) async {
|
||||
final successOps = processed.where((e) => e.success).toSet();
|
||||
final movedOps = successOps.where((e) => !e.skipped).toSet();
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/borders.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
|
@ -9,7 +10,7 @@ class ColorListTile extends StatelessWidget {
|
|||
final Color value;
|
||||
final ValueSetter<Color> onChanged;
|
||||
|
||||
static const double radius = 16.0;
|
||||
static const radius = Constants.colorPickerRadius;
|
||||
|
||||
const ColorListTile({
|
||||
super.key,
|
||||
|
|
|
@ -67,8 +67,8 @@ class OutlinedText extends StatelessWidget {
|
|||
style: (span.style ?? const TextStyle()).copyWith(
|
||||
foreground: Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = outlineWidth
|
||||
..color = outlineColor,
|
||||
..color = outlineColor
|
||||
..strokeWidth = outlineWidth,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
30
lib/widgets/common/identity/aves_fab.dart
Normal file
30
lib/widgets/common/identity/aves_fab.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AvesFab extends StatelessWidget {
|
||||
final String tooltip;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const AvesFab({
|
||||
super.key,
|
||||
required this.tooltip,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TooltipTheme(
|
||||
data: TooltipTheme.of(context).copyWith(
|
||||
preferBelow: false,
|
||||
),
|
||||
child: FloatingActionButton(
|
||||
tooltip: tooltip,
|
||||
onPressed: onPressed,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
child: const Icon(AIcons.apply),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -29,9 +29,9 @@ class CompassPainter extends CustomPainter {
|
|||
..color = color.withOpacity(.6);
|
||||
final strokePaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..color = color
|
||||
..strokeWidth = 1.7
|
||||
..strokeJoin = StrokeJoin.round
|
||||
..color = color;
|
||||
..strokeJoin = StrokeJoin.round;
|
||||
|
||||
canvas.drawPath(northTriangle, fillPaint);
|
||||
canvas.drawPath(northTriangle, strokePaint);
|
||||
|
|
|
@ -172,7 +172,7 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
|
|||
if (widget.cancellableNotifier?.value ?? false) {
|
||||
final key = await _currentProviderStream?.provider.provider.obtainKey(ImageConfiguration.empty);
|
||||
if (key is ThumbnailProviderKey) {
|
||||
mediaFileService.cancelThumbnail(key);
|
||||
mediaFetchService.cancelThumbnail(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ class _DebugCacheSectionState extends State<DebugCacheSection> with AutomaticKee
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: mediaFileService.clearSizedThumbnailDiskCache,
|
||||
onPressed: mediaFetchService.clearSizedThumbnailDiskCache,
|
||||
child: const Text('Clear'),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -64,7 +64,7 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
IconButton(
|
||||
icon: const Icon(AIcons.reset),
|
||||
onPressed: _reset,
|
||||
tooltip: l10n.resetButtonTooltip,
|
||||
tooltip: l10n.resetTooltip,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/services/media/media_file_service.dart';
|
||||
import 'package:aves/services/media/media_edit_service.dart';
|
||||
import 'package:aves/utils/mime_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:aves/model/settings/settings.dart';
|
|||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/basic/color_list_tile.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/borders.dart';
|
||||
|
@ -53,7 +54,7 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
|||
|
||||
static const double itemPickerExtent = 46;
|
||||
static const double appPickerExtent = 32;
|
||||
static const double colorPickerRadius = 16;
|
||||
static const double colorPickerRadius = Constants.colorPickerRadius;
|
||||
|
||||
double tabBarHeight(BuildContext context) => 64 * max(1, MediaQuery.textScaleFactorOf(context));
|
||||
|
||||
|
|
|
@ -250,12 +250,12 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
if (!await checkStoragePermissionForAlbums(context, filledAlbums)) return;
|
||||
|
||||
source.pauseMonitoring();
|
||||
final opId = mediaFileService.newOpId;
|
||||
final opId = mediaEditService.newOpId;
|
||||
await showOpReport<ImageOpEvent>(
|
||||
context: context,
|
||||
opStream: mediaFileService.delete(opId: opId, entries: todoEntries),
|
||||
opStream: mediaEditService.delete(opId: opId, entries: todoEntries),
|
||||
itemCount: todoCount,
|
||||
onCancel: () => mediaFileService.cancelFileOp(opId),
|
||||
onCancel: () => mediaEditService.cancelFileOp(opId),
|
||||
onDone: (processed) async {
|
||||
final successOps = processed.where((event) => event.success);
|
||||
final deletedOps = successOps.where((e) => !e.skipped).toSet();
|
||||
|
@ -314,10 +314,10 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
}
|
||||
|
||||
source.pauseMonitoring();
|
||||
final opId = mediaFileService.newOpId;
|
||||
final opId = mediaEditService.newOpId;
|
||||
await showOpReport<MoveOpEvent>(
|
||||
context: context,
|
||||
opStream: mediaFileService.move(
|
||||
opStream: mediaEditService.move(
|
||||
opId: opId,
|
||||
entriesByDestination: {destinationAlbum: todoEntries},
|
||||
copy: false,
|
||||
|
@ -325,7 +325,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
nameConflictStrategy: NameConflictStrategy.rename,
|
||||
),
|
||||
itemCount: todoCount,
|
||||
onCancel: () => mediaFileService.cancelFileOp(opId),
|
||||
onCancel: () => mediaEditService.cancelFileOp(opId),
|
||||
onDone: (processed) async {
|
||||
final successOps = processed.where((e) => e.success).toSet();
|
||||
final movedOps = successOps.where((e) => !e.skipped).toSet();
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:aves/services/analysis_service.dart';
|
|||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/services/global_search.dart';
|
||||
import 'package:aves/services/intent_service.dart';
|
||||
import 'package:aves/services/widget_service.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/behaviour/routes.dart';
|
||||
|
@ -22,6 +23,7 @@ import 'package:aves/widgets/common/search/route.dart';
|
|||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/settings/screen_saver_settings_page.dart';
|
||||
import 'package:aves/widgets/settings/home_widget_settings_page.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||
import 'package:aves/widgets/viewer/screen_saver_page.dart';
|
||||
import 'package:aves/widgets/wallpaper_page.dart';
|
||||
|
@ -47,6 +49,7 @@ class HomePage extends StatefulWidget {
|
|||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
AvesEntry? _viewerEntry;
|
||||
int? _widgetId;
|
||||
String? _initialRouteName, _initialSearchQuery;
|
||||
Set<CollectionFilter>? _initialFilters;
|
||||
|
||||
|
@ -57,6 +60,17 @@ class _HomePageState extends State<HomePage> {
|
|||
static const actionSearch = 'search';
|
||||
static const actionSetWallpaper = 'set_wallpaper';
|
||||
static const actionView = 'view';
|
||||
static const actionWidgetOpen = 'widget_open';
|
||||
static const actionWidgetSettings = 'widget_settings';
|
||||
|
||||
static const intentDataKeyAction = 'action';
|
||||
static const intentDataKeyAllowMultiple = 'allowMultiple';
|
||||
static const intentDataKeyFilters = 'filters';
|
||||
static const intentDataKeyMimeType = 'mimeType';
|
||||
static const intentDataKeyPage = 'page';
|
||||
static const intentDataKeyQuery = 'query';
|
||||
static const intentDataKeyUri = 'uri';
|
||||
static const intentDataKeyWidgetId = 'widgetId';
|
||||
|
||||
static const allowedShortcutRoutes = [
|
||||
CollectionPage.routeName,
|
||||
|
@ -88,7 +102,7 @@ class _HomePageState extends State<HomePage> {
|
|||
|
||||
var appMode = AppMode.main;
|
||||
final intentData = widget.intentData ?? await IntentService.getIntentData();
|
||||
final intentAction = intentData['action'];
|
||||
final intentAction = intentData[intentDataKeyAction];
|
||||
|
||||
if (!{actionScreenSaver, actionSetWallpaper}.contains(intentAction)) {
|
||||
await androidFileUtils.init();
|
||||
|
@ -101,19 +115,31 @@ class _HomePageState extends State<HomePage> {
|
|||
await reportService.log('Intent data=$intentData');
|
||||
switch (intentAction) {
|
||||
case actionView:
|
||||
_viewerEntry = await _initViewerEntry(
|
||||
uri: intentData['uri'],
|
||||
mimeType: intentData['mimeType'],
|
||||
);
|
||||
if (_viewerEntry != null) {
|
||||
appMode = AppMode.view;
|
||||
case actionWidgetOpen:
|
||||
String? uri, mimeType;
|
||||
final widgetId = intentData[intentDataKeyWidgetId];
|
||||
if (widgetId != null) {
|
||||
uri = settings.getWidgetUri(widgetId);
|
||||
unawaited(WidgetService.update(widgetId));
|
||||
} else {
|
||||
uri = intentData[intentDataKeyUri];
|
||||
mimeType = intentData[intentDataKeyMimeType];
|
||||
}
|
||||
if (uri != null) {
|
||||
_viewerEntry = await _initViewerEntry(
|
||||
uri: uri,
|
||||
mimeType: mimeType,
|
||||
);
|
||||
if (_viewerEntry != null) {
|
||||
appMode = AppMode.view;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case actionPickItems:
|
||||
// TODO TLAD apply pick mimetype(s)
|
||||
// some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?)
|
||||
String? pickMimeTypes = intentData['mimeType'];
|
||||
final multiple = intentData['allowMultiple'] ?? false;
|
||||
String? pickMimeTypes = intentData[intentDataKeyMimeType];
|
||||
final multiple = intentData[intentDataKeyAllowMultiple] ?? false;
|
||||
debugPrint('pick mimeType=$pickMimeTypes multiple=$multiple');
|
||||
appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal;
|
||||
break;
|
||||
|
@ -129,23 +155,27 @@ class _HomePageState extends State<HomePage> {
|
|||
break;
|
||||
case actionSearch:
|
||||
_initialRouteName = CollectionSearchDelegate.pageRouteName;
|
||||
_initialSearchQuery = intentData['query'];
|
||||
_initialSearchQuery = intentData[intentDataKeyQuery];
|
||||
break;
|
||||
case actionSetWallpaper:
|
||||
appMode = AppMode.setWallpaper;
|
||||
_viewerEntry = await _initViewerEntry(
|
||||
uri: intentData['uri'],
|
||||
mimeType: intentData['mimeType'],
|
||||
uri: intentData[intentDataKeyUri],
|
||||
mimeType: intentData[intentDataKeyMimeType],
|
||||
);
|
||||
break;
|
||||
case actionWidgetSettings:
|
||||
_initialRouteName = HomeWidgetSettingsPage.routeName;
|
||||
_widgetId = intentData[intentDataKeyWidgetId] ?? 0;
|
||||
break;
|
||||
default:
|
||||
// do not use 'route' as extra key, as the Flutter framework acts on it
|
||||
final extraRoute = intentData['page'];
|
||||
final extraRoute = intentData[intentDataKeyPage];
|
||||
if (allowedShortcutRoutes.contains(extraRoute)) {
|
||||
_initialRouteName = extraRoute;
|
||||
}
|
||||
}
|
||||
final extraFilters = intentData['filters'];
|
||||
final extraFilters = intentData[intentDataKeyFilters];
|
||||
_initialFilters = extraFilters != null ? (extraFilters as List).cast<String>().map(CollectionFilter.fromJson).whereNotNull().toSet() : null;
|
||||
}
|
||||
context.read<ValueNotifier<AppMode>>().value = appMode;
|
||||
|
@ -207,7 +237,7 @@ class _HomePageState extends State<HomePage> {
|
|||
// convert this file path to a proper URI
|
||||
uri = Uri.file(uri).toString();
|
||||
}
|
||||
final entry = await mediaFileService.getEntry(uri, mimeType);
|
||||
final entry = await mediaFetchService.getEntry(uri, mimeType);
|
||||
if (entry != null) {
|
||||
// cataloguing is essential for coordinates and video rotation
|
||||
await entry.catalog(background: false, force: false, persist: false);
|
||||
|
@ -309,6 +339,13 @@ class _HomePageState extends State<HomePage> {
|
|||
settings: RouteSettings(name: routeName),
|
||||
builder: (context) => const ScreenSaverSettingsPage(),
|
||||
);
|
||||
case HomeWidgetSettingsPage.routeName:
|
||||
return DirectMaterialPageRoute(
|
||||
settings: RouteSettings(name: routeName),
|
||||
builder: (context) => HomeWidgetSettingsPage(
|
||||
widgetId: _widgetId!,
|
||||
),
|
||||
);
|
||||
case CollectionSearchDelegate.pageRouteName:
|
||||
return SearchPageRoute(
|
||||
delegate: CollectionSearchDelegate(
|
||||
|
|
86
lib/widgets/home_widget.dart
Normal file
86
lib/widgets/home_widget.dart
Normal file
|
@ -0,0 +1,86 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/entry_images.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/model/settings/enums/widget_shape.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HomeWidgetPainter {
|
||||
final AvesEntry? entry;
|
||||
final double devicePixelRatio;
|
||||
|
||||
static const backgroundGradient = LinearGradient(
|
||||
begin: Alignment.bottomLeft,
|
||||
end: Alignment.topRight,
|
||||
colors: Constants.boraBoraGradientColors,
|
||||
);
|
||||
|
||||
HomeWidgetPainter({
|
||||
required this.entry,
|
||||
required this.devicePixelRatio,
|
||||
});
|
||||
|
||||
Future<Uint8List> drawWidget({
|
||||
required int widthPx,
|
||||
required int heightPx,
|
||||
required Color? outline,
|
||||
required WidgetShape shape,
|
||||
ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba,
|
||||
}) async {
|
||||
final widgetSizePx = Size(widthPx.toDouble(), heightPx.toDouble());
|
||||
final entryImage = await _getEntryImage(entry, shape.size(widgetSizePx));
|
||||
|
||||
final recorder = ui.PictureRecorder();
|
||||
final rect = Rect.fromLTWH(0, 0, widgetSizePx.width, widgetSizePx.height);
|
||||
final canvas = Canvas(recorder, rect);
|
||||
final path = shape.path(widgetSizePx, devicePixelRatio);
|
||||
canvas.clipPath(path);
|
||||
if (entryImage != null) {
|
||||
canvas.drawImage(entryImage, Offset(widgetSizePx.width - entryImage.width, widgetSizePx.height - entryImage.height) / 2, Paint());
|
||||
} else {
|
||||
canvas.drawPaint(Paint()..shader = backgroundGradient.createShader(rect));
|
||||
}
|
||||
if (outline != null) {
|
||||
drawOutline(canvas, path, devicePixelRatio, outline);
|
||||
}
|
||||
final widgetImage = await recorder.endRecording().toImage(widthPx, heightPx);
|
||||
final byteData = await widgetImage.toByteData(format: format);
|
||||
return byteData?.buffer.asUint8List() ?? Uint8List(0);
|
||||
}
|
||||
|
||||
static void drawOutline(ui.Canvas canvas, ui.Path outlinePath, double devicePixelRatio, Color color) {
|
||||
canvas.drawPath(
|
||||
outlinePath,
|
||||
Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..color = color
|
||||
..strokeWidth = AvesFilterChip.outlineWidth * devicePixelRatio * 2
|
||||
..strokeCap = StrokeCap.round);
|
||||
}
|
||||
|
||||
FutureOr<ui.Image?> _getEntryImage(AvesEntry? entry, Size sizePx) async {
|
||||
if (entry == null) return null;
|
||||
|
||||
final provider = entry.getThumbnail(extent: sizePx.longestSide / devicePixelRatio);
|
||||
|
||||
final imageInfoCompleter = Completer<ImageInfo?>();
|
||||
final imageStream = provider.resolve(ImageConfiguration.empty);
|
||||
final imageStreamListener = ImageStreamListener((image, synchronousCall) async {
|
||||
imageInfoCompleter.complete(image);
|
||||
}, onError: imageInfoCompleter.completeError);
|
||||
imageStream.addListener(imageStreamListener);
|
||||
ImageInfo? regionImageInfo;
|
||||
try {
|
||||
regionImageInfo = await imageInfoCompleter.future;
|
||||
} catch (error) {
|
||||
debugPrint('failed to get widget image for entry=$entry with error=$error');
|
||||
}
|
||||
imageStream.removeListener(imageStreamListener);
|
||||
return regionImageInfo?.image;
|
||||
}
|
||||
}
|
70
lib/widgets/settings/common/collection_tile.dart
Normal file
70
lib/widgets/settings/common/collection_tile.dart
Normal file
|
@ -0,0 +1,70 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/services/intent_service.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/collection/filter_bar.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsCollectionTile extends StatelessWidget {
|
||||
final Set<CollectionFilter> filters;
|
||||
final void Function(Set<CollectionFilter>) onSelection;
|
||||
|
||||
const SettingsCollectionTile({
|
||||
super.key,
|
||||
required this.filters,
|
||||
required this.onSelection,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final hasSubtitle = filters.isEmpty;
|
||||
|
||||
// size and padding to match `ListTile`
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: (hasSubtitle ? 72.0 : 56.0) + theme.visualDensity.baseSizeAdjustment.dy,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.collectionPageTitle,
|
||||
style: textTheme.subtitle1!,
|
||||
),
|
||||
if (hasSubtitle)
|
||||
Text(
|
||||
l10n.drawerCollectionAll,
|
||||
style: textTheme.bodyText2!.copyWith(color: textTheme.caption!.color),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final selection = await IntentService.pickCollectionFilters(filters);
|
||||
if (selection != null) {
|
||||
onSelection(selection);
|
||||
}
|
||||
},
|
||||
icon: const Icon(AIcons.edit),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (filters.isNotEmpty) FilterBar(filters: filters),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
233
lib/widgets/settings/home_widget_settings_page.dart
Normal file
233
lib/widgets/settings/home_widget_settings_page.dart
Normal file
|
@ -0,0 +1,233 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/model/settings/enums/widget_shape.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/services/widget_service.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/borders.dart';
|
||||
import 'package:aves/widgets/common/identity/buttons.dart';
|
||||
import 'package:aves/widgets/home_widget.dart';
|
||||
import 'package:aves/widgets/settings/common/collection_tile.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class HomeWidgetSettingsPage extends StatefulWidget {
|
||||
static const routeName = '/settings/home_widget';
|
||||
|
||||
final int widgetId;
|
||||
|
||||
const HomeWidgetSettingsPage({
|
||||
super.key,
|
||||
required this.widgetId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<HomeWidgetSettingsPage> createState() => _HomeWidgetSettingsPageState();
|
||||
}
|
||||
|
||||
class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
||||
late Color? _outline;
|
||||
late WidgetShape _shape;
|
||||
late Set<CollectionFilter> _collectionFilters;
|
||||
|
||||
int get widgetId => widget.widgetId;
|
||||
|
||||
static const gradient = HomeWidgetPainter.backgroundGradient;
|
||||
static final deselectedGradient = LinearGradient(
|
||||
begin: gradient.begin,
|
||||
end: gradient.end,
|
||||
colors: gradient.colors.map((v) {
|
||||
final l = (v.computeLuminance() * 0xFF).toInt();
|
||||
return Color.fromARGB(0xFF, l, l, l);
|
||||
}).toList(),
|
||||
stops: gradient.stops,
|
||||
tileMode: gradient.tileMode,
|
||||
transform: gradient.transform,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_outline = settings.getWidgetOutline(widgetId);
|
||||
_shape = settings.getWidgetShape(widgetId);
|
||||
_collectionFilters = settings.getWidgetCollectionFilters(widgetId);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.settingsWidgetPageTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
_buildShapeSelector(),
|
||||
ListTile(
|
||||
title: Text(l10n.settingsWidgetShowOutline),
|
||||
trailing: HomeWidgetOutlineSelector(
|
||||
getter: () => _outline,
|
||||
setter: (v) => setState(() => _outline = v),
|
||||
),
|
||||
),
|
||||
SettingsCollectionTile(
|
||||
filters: _collectionFilters,
|
||||
onSelection: (v) => setState(() => _collectionFilters = v),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: AvesOutlinedButton(
|
||||
label: l10n.saveTooltip,
|
||||
onPressed: () {
|
||||
_saveSettings();
|
||||
WidgetService.configure();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShapeSelector() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
child: Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: WidgetShape.values.map((shape) {
|
||||
final selected = shape == _shape;
|
||||
final duration = context.read<DurationsData>().formTransition;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _shape = shape),
|
||||
child: AnimatedOpacity(
|
||||
duration: duration,
|
||||
opacity: selected ? 1.0 : .4,
|
||||
child: AnimatedContainer(
|
||||
duration: duration,
|
||||
width: 96,
|
||||
height: 124,
|
||||
decoration: ShapeDecoration(
|
||||
gradient: selected ? gradient : deselectedGradient,
|
||||
shape: _WidgetShapeBorder(_outline, shape),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _saveSettings() {
|
||||
settings.setWidgetOutline(widgetId, _outline);
|
||||
settings.setWidgetShape(widgetId, _shape);
|
||||
if (!const SetEquality().equals(_collectionFilters, settings.getWidgetCollectionFilters(widgetId))) {
|
||||
settings.setWidgetCollectionFilters(widgetId, _collectionFilters);
|
||||
settings.setWidgetUri(widgetId, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _WidgetShapeBorder extends ShapeBorder {
|
||||
final Color? outline;
|
||||
final WidgetShape shape;
|
||||
|
||||
static const _devicePixelRatio = 1.0;
|
||||
|
||||
const _WidgetShapeBorder(this.outline, this.shape);
|
||||
|
||||
@override
|
||||
EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
|
||||
|
||||
@override
|
||||
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
|
||||
return getOuterPath(rect, textDirection: textDirection);
|
||||
}
|
||||
|
||||
@override
|
||||
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
|
||||
return shape.path(rect.size, _devicePixelRatio);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
|
||||
if (outline != null) {
|
||||
final path = shape.path(rect.size, _devicePixelRatio);
|
||||
canvas.clipPath(path);
|
||||
HomeWidgetPainter.drawOutline(canvas, path, _devicePixelRatio, outline!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ShapeBorder scale(double t) => this;
|
||||
}
|
||||
|
||||
class HomeWidgetOutlineSelector extends StatefulWidget {
|
||||
final ValueGetter<Color?> getter;
|
||||
final ValueSetter<Color?> setter;
|
||||
|
||||
const HomeWidgetOutlineSelector({
|
||||
super.key,
|
||||
required this.getter,
|
||||
required this.setter,
|
||||
});
|
||||
|
||||
@override
|
||||
State<HomeWidgetOutlineSelector> createState() => _HomeWidgetOutlineSelectorState();
|
||||
}
|
||||
|
||||
class _HomeWidgetOutlineSelectorState extends State<HomeWidgetOutlineSelector> {
|
||||
static const radius = Constants.colorPickerRadius;
|
||||
static const List<Color?> options = [
|
||||
null,
|
||||
Colors.black,
|
||||
Colors.white,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButtonHideUnderline(
|
||||
child: DropdownButton<Color?>(
|
||||
items: _buildItems(context),
|
||||
value: widget.getter(),
|
||||
onChanged: (selected) {
|
||||
widget.setter(selected);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<DropdownMenuItem<Color?>> _buildItems(BuildContext context) {
|
||||
return options.map((selected) {
|
||||
return DropdownMenuItem<Color?>(
|
||||
value: selected,
|
||||
child: Container(
|
||||
height: radius * 2,
|
||||
width: radius * 2,
|
||||
decoration: BoxDecoration(
|
||||
color: selected,
|
||||
border: AvesBorder.border(context),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: selected == null ? const Icon(AIcons.clear) : null,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
}
|
|
@ -4,10 +4,8 @@ import 'package:aves/model/settings/enums/slideshow_interval.dart';
|
|||
import 'package:aves/model/settings/enums/slideshow_video_playback.dart';
|
||||
import 'package:aves/model/settings/enums/viewer_transition.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/services/intent_service.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/collection/filter_bar.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/settings/common/collection_tile.dart';
|
||||
import 'package:aves/widgets/settings/common/tiles.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -22,7 +20,7 @@ class ScreenSaverSettingsPage extends StatelessWidget {
|
|||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.settingsScreenSaverTitle),
|
||||
title: Text(l10n.settingsScreenSaverPageTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
|
@ -59,53 +57,9 @@ class ScreenSaverSettingsPage extends StatelessWidget {
|
|||
Selector<Settings, Set<CollectionFilter>>(
|
||||
selector: (context, s) => s.screenSaverCollectionFilters,
|
||||
builder: (context, filters, child) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final hasSubtitle = filters.isEmpty;
|
||||
|
||||
// size and padding to match `ListTile`
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: (hasSubtitle ? 72.0 : 56.0) + theme.visualDensity.baseSizeAdjustment.dy,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.collectionPageTitle,
|
||||
style: textTheme.subtitle1!,
|
||||
),
|
||||
if (hasSubtitle)
|
||||
Text(
|
||||
l10n.drawerCollectionAll,
|
||||
style: textTheme.bodyText2!.copyWith(color: textTheme.caption!.color),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final selection = await IntentService.pickCollectionFilters(filters);
|
||||
if (selection != null) {
|
||||
settings.screenSaverCollectionFilters = selection;
|
||||
}
|
||||
},
|
||||
icon: const Icon(AIcons.edit),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (filters.isNotEmpty) FilterBar(filters: filters),
|
||||
],
|
||||
),
|
||||
),
|
||||
return SettingsCollectionTile(
|
||||
filters: filters,
|
||||
onSelection: (v) => settings.screenSaverCollectionFilters = v,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/basic/outlined_text.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/borders.dart';
|
||||
|
@ -26,11 +27,7 @@ class SubtitleSample extends StatelessWidget {
|
|||
gradient: const LinearGradient(
|
||||
begin: Alignment.bottomLeft,
|
||||
end: Alignment.topRight,
|
||||
colors: [
|
||||
// Bora Bora
|
||||
Color(0xff2bc0e4),
|
||||
Color(0xffeaecc6),
|
||||
],
|
||||
colors: Constants.boraBoraGradientColors,
|
||||
),
|
||||
border: AvesBorder.border(context),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:aves/model/settings/enums/entry_background.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/fx/borders.dart';
|
||||
import 'package:aves/widgets/common/fx/checkered_decoration.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -19,6 +20,8 @@ class EntryBackgroundSelector extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _EntryBackgroundSelectorState extends State<EntryBackgroundSelector> {
|
||||
static const radius = Constants.colorPickerRadius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButtonHideUnderline(
|
||||
|
@ -36,7 +39,6 @@ class _EntryBackgroundSelectorState extends State<EntryBackgroundSelector> {
|
|||
}
|
||||
|
||||
List<DropdownMenuItem<EntryBackground>> _buildItems(BuildContext context) {
|
||||
const radius = 12.0;
|
||||
return [
|
||||
EntryBackground.white,
|
||||
EntryBackground.black,
|
||||
|
|
|
@ -14,7 +14,7 @@ import 'package:aves/model/source/collection_source.dart';
|
|||
import 'package:aves/services/common/image_op_events.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/services/media/enums.dart';
|
||||
import 'package:aves/services/media/media_file_service.dart';
|
||||
import 'package:aves/services/media/media_edit_service.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/aves_app.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
|
@ -246,7 +246,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
source.pauseMonitoring();
|
||||
await showOpReport<ExportOpEvent>(
|
||||
context: context,
|
||||
opStream: mediaFileService.export(
|
||||
opStream: mediaEditService.export(
|
||||
selection,
|
||||
options: options,
|
||||
destinationAlbum: destinationAlbum,
|
||||
|
@ -336,7 +336,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: SourceViewerPage.routeName),
|
||||
builder: (context) => SourceViewerPage(
|
||||
loader: () => mediaFileService.getSvg(entry.uri, entry.mimeType).then(utf8.decode),
|
||||
loader: () => mediaFetchService.getSvg(entry.uri, entry.mimeType).then(utf8.decode),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -74,7 +74,7 @@ class EntryPrinter with FeedbackMixin {
|
|||
|
||||
Future<pdf.Widget?> _buildPageImage(AvesEntry entry) async {
|
||||
if (entry.isSvg) {
|
||||
final bytes = await mediaFileService.getSvg(entry.uri, entry.mimeType);
|
||||
final bytes = await mediaFetchService.getSvg(entry.uri, entry.mimeType);
|
||||
if (bytes.isNotEmpty) {
|
||||
return pdf.SvgImage(svg: utf8.decode(bytes));
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/entry_images.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/home_widget.dart';
|
||||
import 'package:aves/widgets/viewer/debug/db.dart';
|
||||
import 'package:aves/widgets/viewer/debug/metadata.dart';
|
||||
import 'package:aves/widgets/viewer/info/common.dart';
|
||||
|
@ -27,6 +32,7 @@ class ViewerDebugPage extends StatelessWidget {
|
|||
if (context.select<ValueNotifier<AppMode>, bool>((vn) => vn.value != AppMode.view)) Tuple2(const Tab(text: 'DB'), DbTab(entry: entry)),
|
||||
Tuple2(const Tab(icon: Icon(AIcons.android)), MetadataTab(entry: entry)),
|
||||
Tuple2(const Tab(icon: Icon(AIcons.image)), _buildThumbnailsTabView()),
|
||||
Tuple2(const Tab(icon: Icon(AIcons.addShortcut)), _buildWidgetTabView()),
|
||||
];
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
|
@ -155,7 +161,7 @@ class ViewerDebugPage extends StatelessWidget {
|
|||
padding: const EdgeInsets.all(16),
|
||||
children: entry.cachedThumbnails
|
||||
.expand((provider) => [
|
||||
Text('Extent: ${provider.key.extent}'),
|
||||
Text('Thumb extent: ${provider.key.extent}'),
|
||||
Center(
|
||||
child: Image(
|
||||
image: provider,
|
||||
|
@ -177,4 +183,32 @@ class ViewerDebugPage extends StatelessWidget {
|
|||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWidgetTabView() {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [303, 636, 972, 1305]
|
||||
.expand((heightPx) => [
|
||||
Text('Widget heightPx: $heightPx'),
|
||||
FutureBuilder<Uint8List>(
|
||||
future: HomeWidgetPainter(
|
||||
entry: entry,
|
||||
devicePixelRatio: ui.window.devicePixelRatio,
|
||||
).drawWidget(
|
||||
widthPx: 978,
|
||||
heightPx: heightPx,
|
||||
outline: Colors.amber,
|
||||
shape: WidgetShape.heart,
|
||||
format: ui.ImageByteFormat.png,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final bytes = snapshot.data;
|
||||
if (bytes == null) return const SizedBox();
|
||||
return Image.memory(bytes);
|
||||
},
|
||||
),
|
||||
])
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
}
|
||||
};
|
||||
|
||||
final newFields = await mediaFileService.captureFrame(
|
||||
final newFields = await mediaEditService.captureFrame(
|
||||
entry,
|
||||
desiredName: '${entry.bestTitle}_${'$positionMillis'.padLeft(8, '0')}',
|
||||
exif: exif,
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/services/common/image_op_events.dart';
|
||||
import 'package:aves/services/media/media_file_service.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'media_store_service.dart';
|
||||
|
||||
class FakeMediaFileService extends Fake implements MediaFileService {
|
||||
@override
|
||||
Stream<MoveOpEvent> rename({
|
||||
String? opId,
|
||||
required Map<AvesEntry, String> entriesToNewName,
|
||||
}) {
|
||||
final contentId = FakeMediaStoreService.nextId;
|
||||
final kv = entriesToNewName.entries.first;
|
||||
final entry = kv.key;
|
||||
final newName = kv.value;
|
||||
return Stream.value(MoveOpEvent(
|
||||
success: true,
|
||||
skipped: false,
|
||||
uri: entry.uri,
|
||||
newFields: {
|
||||
'uri': 'content://media/external/images/media/$contentId',
|
||||
'contentId': contentId,
|
||||
'path': '${entry.directory}/$newName',
|
||||
'dateModifiedSecs': FakeMediaStoreService.dateSecs,
|
||||
},
|
||||
deleted: false,
|
||||
));
|
||||
}
|
||||
}
|
|
@ -15,7 +15,6 @@ import 'package:aves/model/source/media_store_source.dart';
|
|||
import 'package:aves/services/android_app_service.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/services/device_service.dart';
|
||||
import 'package:aves/services/media/media_file_service.dart';
|
||||
import 'package:aves/services/media/media_store_service.dart';
|
||||
import 'package:aves/services/metadata/metadata_fetch_service.dart';
|
||||
import 'package:aves/services/storage_service.dart';
|
||||
|
@ -30,7 +29,6 @@ import 'package:path/path.dart' as p;
|
|||
import '../fake/android_app_service.dart';
|
||||
import '../fake/availability.dart';
|
||||
import '../fake/device_service.dart';
|
||||
import '../fake/media_file_service.dart';
|
||||
import '../fake/media_store_service.dart';
|
||||
import '../fake/metadata_db.dart';
|
||||
import '../fake/metadata_fetch_service.dart';
|
||||
|
@ -59,7 +57,6 @@ void main() {
|
|||
|
||||
getIt.registerLazySingleton<AndroidAppService>(FakeAndroidAppService.new);
|
||||
getIt.registerLazySingleton<DeviceService>(FakeDeviceService.new);
|
||||
getIt.registerLazySingleton<MediaFileService>(FakeMediaFileService.new);
|
||||
getIt.registerLazySingleton<MediaStoreService>(FakeMediaStoreService.new);
|
||||
getIt.registerLazySingleton<MetadataFetchService>(FakeMetadataFetchService.new);
|
||||
getIt.registerLazySingleton<ReportService>(FakeReportService.new);
|
||||
|
|
|
@ -2,55 +2,73 @@
|
|||
"de": [
|
||||
"filterOnThisDayLabel",
|
||||
"settingsSlideshowFillScreen",
|
||||
"settingsScreenSaverTitle"
|
||||
"settingsScreenSaverPageTitle",
|
||||
"settingsWidgetPageTitle",
|
||||
"settingsWidgetShowOutline"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"filterOnThisDayLabel",
|
||||
"settingsSlideshowFillScreen",
|
||||
"settingsScreenSaverTitle"
|
||||
"settingsScreenSaverPageTitle",
|
||||
"settingsWidgetPageTitle",
|
||||
"settingsWidgetShowOutline"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"filterOnThisDayLabel",
|
||||
"settingsSlideshowFillScreen",
|
||||
"settingsScreenSaverTitle"
|
||||
"settingsScreenSaverPageTitle",
|
||||
"settingsWidgetPageTitle",
|
||||
"settingsWidgetShowOutline"
|
||||
],
|
||||
|
||||
"id": [
|
||||
"filterOnThisDayLabel",
|
||||
"settingsSlideshowFillScreen",
|
||||
"settingsScreenSaverTitle"
|
||||
"settingsScreenSaverPageTitle",
|
||||
"settingsWidgetPageTitle",
|
||||
"settingsWidgetShowOutline"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"filterOnThisDayLabel",
|
||||
"settingsSlideshowFillScreen",
|
||||
"settingsScreenSaverTitle"
|
||||
"settingsScreenSaverPageTitle",
|
||||
"settingsWidgetPageTitle",
|
||||
"settingsWidgetShowOutline"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"filterOnThisDayLabel",
|
||||
"settingsSlideshowFillScreen",
|
||||
"settingsScreenSaverTitle"
|
||||
"settingsScreenSaverPageTitle",
|
||||
"settingsWidgetPageTitle",
|
||||
"settingsWidgetShowOutline"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
"filterOnThisDayLabel",
|
||||
"settingsSlideshowFillScreen",
|
||||
"settingsScreenSaverTitle"
|
||||
"settingsScreenSaverPageTitle",
|
||||
"settingsWidgetPageTitle",
|
||||
"settingsWidgetShowOutline"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"filterOnThisDayLabel",
|
||||
"settingsSlideshowFillScreen",
|
||||
"settingsScreenSaverTitle"
|
||||
"settingsScreenSaverPageTitle",
|
||||
"settingsWidgetPageTitle",
|
||||
"settingsWidgetShowOutline"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"filterOnThisDayLabel",
|
||||
"settingsSlideshowFillScreen",
|
||||
"settingsScreenSaverTitle"
|
||||
"settingsScreenSaverPageTitle",
|
||||
"settingsWidgetPageTitle",
|
||||
"settingsWidgetShowOutline"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
|
@ -79,13 +97,17 @@
|
|||
"settingsSlideshowIntervalTitle",
|
||||
"settingsSlideshowVideoPlaybackTile",
|
||||
"settingsSlideshowVideoPlaybackTitle",
|
||||
"settingsScreenSaverTitle",
|
||||
"settingsScreenSaverPageTitle",
|
||||
"settingsWidgetPageTitle",
|
||||
"settingsWidgetShowOutline",
|
||||
"viewerSetWallpaperButtonLabel"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"filterOnThisDayLabel",
|
||||
"settingsSlideshowFillScreen",
|
||||
"settingsScreenSaverTitle"
|
||||
"settingsScreenSaverPageTitle",
|
||||
"settingsWidgetPageTitle",
|
||||
"settingsWidgetShowOutline"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue