diff --git a/.flutter b/.flutter index 4d9e56e69..9cd3d0d9f 160000 --- a/.flutter +++ b/.flutter @@ -1 +1 @@ -Subproject commit 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf +Subproject commit 9cd3d0d9ff05768afa249e036acc66e8abe93bff diff --git a/CHANGELOG.md b/CHANGELOG.md index 11ac2cb77..e71f00321 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [v1.8.7] - 2023-05-26 + +### Added + +- option to set the Tags page as home +- support for animated PNG (requires rescan) +- Info: added day filter with item date +- Widget: option to update image on tap +- Slideshow / Screen saver: option for random transition +- Norwegian (Nynorsk) translation (thanks tryvseu) + +### Changed + +- keep showing empty albums if are pinned +- remember whether to show the title filter when picking albums +- upgraded Flutter to stable v3.10.2 + +### Fixed + +- crash when cataloguing PSD with large XMP +- crash when cataloguing large HEIF + ## [v1.8.6] - 2023-04-30 ### Added @@ -330,8 +352,8 @@ All notable changes to this project will be documented in this file. - Albums / Countries / Tags: live title filter - option to hide confirmation message after moving items to the bin - Collection / Info: edit description via Exif / IPTC / XMP -- Info: read XMP from HEIC on Android >=11 -- Collection: support HEIC motion photos on Android >=11 +- Info: read XMP from HEIF on Android >=11 +- Collection: support HEIF motion photos on Android >=11 - Search: `recently added` filter - Dutch translation (thanks Martijn Fabrie, Koen Koppens) @@ -742,7 +764,7 @@ All notable changes to this project will be documented in this file. ### Fixed - auto album identification and naming -- opening HEIC images from downloads content URI on Android >=11 +- opening HEIF images from downloads content URI on Android >=11 ## [v1.4.7] - 2021-08-06 [YANKED] @@ -837,7 +859,7 @@ All notable changes to this project will be documented in this file. ### Added - Motion photo support -- Viewer: play videos in multi-track HEIC +- Viewer: play videos in multi-track HEIF - Handle share intent ### Changed @@ -846,7 +868,7 @@ All notable changes to this project will be documented in this file. ### Fixed -- fixed crash when cataloguing large MP4/PSD +- crash when cataloguing large MP4/PSD - prevent videos playing in the background when quickly switching entries ## [v1.4.0] - 2021-04-16 @@ -964,7 +986,7 @@ All notable changes to this project will be documented in this file. ### Added -Collection: identify multipage TIFF & multitrack HEIC/HEIF Viewer: support for multipage TIFF +Collection: identify multipage TIFF & multitrack HEIF Viewer: support for multipage TIFF Viewer: support for cropped panoramas Albums: grouping options ### Changed @@ -1075,7 +1097,7 @@ upgraded libtiff to 4.2.0 for TIFF decoding - Viewer: leave when the loaded item is deleted and it is the last one - Viewer: refresh the viewer overlay and info page when the loaded image is modified -- Info: prevent reporting a "Media" section for images other than HEIC/HEIF +- Info: prevent reporting a "Media" section for images other than HEIF - Fixed opening items shared via a "file" media content URI ### Removed diff --git a/android/app/build.gradle b/android/app/build.gradle index 575f5c986..bc24d307d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { id 'com.android.application' id 'kotlin-android' @@ -36,17 +38,17 @@ if (keystorePropertiesFile.exists()) { // for release using credentials in environment variables set up by GitHub Actions // warning: in property file, single quotes should be escaped with a backslash // but they should not be escaped when stored in env variables - keystoreProperties['storeFile'] = System.getenv('AVES_STORE_FILE') ?: '' - keystoreProperties['storePassword'] = System.getenv('AVES_STORE_PASSWORD') ?: '' - keystoreProperties['keyAlias'] = System.getenv('AVES_KEY_ALIAS') ?: '' - keystoreProperties['keyPassword'] = System.getenv('AVES_KEY_PASSWORD') ?: '' - keystoreProperties['googleApiKey'] = System.getenv('AVES_GOOGLE_API_KEY') ?: '' - keystoreProperties['huaweiApiKey'] = System.getenv('AVES_HUAWEI_API_KEY') ?: '' + keystoreProperties["storeFile"] = System.getenv("AVES_STORE_FILE") ?: "" + keystoreProperties["storePassword"] = System.getenv("AVES_STORE_PASSWORD") ?: "" + keystoreProperties["keyAlias"] = System.getenv("AVES_KEY_ALIAS") ?: "" + keystoreProperties["keyPassword"] = System.getenv("AVES_KEY_PASSWORD") ?: "" + keystoreProperties["googleApiKey"] = System.getenv("AVES_GOOGLE_API_KEY") ?: "" + keystoreProperties["huaweiApiKey"] = System.getenv("AVES_HUAWEI_API_KEY") ?: "" } android { namespace 'deckers.thibault.aves' - compileSdkVersion 33 + compileSdk 33 ndkVersion flutter.ndkVersion compileOptions { @@ -60,10 +62,6 @@ android { disable 'InvalidPackage' } - kotlinOptions { - jvmTarget = '1.8' - } - sourceSets { main.java.srcDirs += 'src/main/kotlin' } @@ -80,21 +78,21 @@ android { targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName - manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey'] ?: '', - huaweiApiKey: keystoreProperties['huaweiApiKey'] ?: ''] + manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "", + huaweiApiKey: keystoreProperties["huaweiApiKey"] ?: ""] multiDexEnabled true } signingConfigs { release { - keyAlias keystoreProperties['keyAlias'] - keyPassword keystoreProperties['keyPassword'] - storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null - storePassword keystoreProperties['storePassword'] + keyAlias keystoreProperties["keyAlias"] + keyPassword keystoreProperties["keyPassword"] + storeFile keystoreProperties["storeFile"] ? file(keystoreProperties["storeFile"]) : null + storePassword keystoreProperties["storePassword"] } } - flavorDimensions "store" + flavorDimensions = ["store"] productFlavors { play { @@ -174,6 +172,15 @@ android { } } +tasks.withType(KotlinCompile).configureEach { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + + kotlinOptions { + jvmTarget = '1.8' + } +} + flutter { source '../..' } @@ -195,20 +202,21 @@ repositories { } dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' implementation "androidx.appcompat:appcompat:1.6.1" - implementation 'androidx.core:core-ktx:1.10.0' + implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.exifinterface:exifinterface:1.3.6' implementation 'androidx.lifecycle:lifecycle-process:2.6.1' implementation 'androidx.media:media:1.6.0' implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.security:security-crypto:1.1.0-alpha06' + implementation 'androidx.work:work-runtime-ktx:2.8.1' implementation 'com.caverock:androidsvg-aar:1.4' implementation 'com.commonsware.cwac:document:0.5.0' implementation 'com.drewnoakes:metadata-extractor:2.18.0' - implementation 'com.github.bumptech.glide:glide:4.15.1' + implementation "com.github.bumptech.glide:glide:$glide_version" // SLF4J implementation for `mp4parser` implementation 'org.slf4j:slf4j-simple:2.0.7' @@ -217,15 +225,17 @@ dependencies { // - https://jitpack.io/p/deckerst/mp4parser // - https://jitpack.io/p/deckerst/pixymeta-android implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a' - implementation 'com.github.deckerst.mp4parser:isoparser:42f2cdc087' - implementation 'com.github.deckerst.mp4parser:muxer:42f2cdc087' + implementation 'com.github.deckerst.mp4parser:isoparser:b7b853f2e3' + implementation 'com.github.deckerst.mp4parser:muxer:b7b853f2e3' implementation 'com.github.deckerst:pixymeta-android:706bd73d6e' // huawei flavor only - huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.8.0.300' + huaweiImplementation "com.huawei.agconnect:agconnect-core:$huawei_agconnect_version" + + testImplementation "org.junit.jupiter:junit-jupiter-engine:5.9.2" kapt 'androidx.annotation:annotation:1.6.0' - kapt 'com.github.bumptech.glide:compiler:4.15.1' + kapt "com.github.bumptech.glide:compiler:$glide_version" compileOnly rootProject.findProject(':streams_channel') } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5b85aa256..a8084c988 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,14 +1,7 @@ - + + @@ -242,11 +248,6 @@ This change eventually prevents building the app with Flutter v3.7.11. - - - + diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt deleted file mode 100644 index e5e4e7a60..000000000 --- a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt +++ /dev/null @@ -1,258 +0,0 @@ -package deckers.thibault.aves - -import android.app.Notification -import android.app.PendingIntent -import android.app.Service -import android.content.Context -import android.content.Intent -import android.os.* -import android.util.Log -import androidx.core.app.NotificationChannelCompat -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import app.loup.streams_channel.StreamsChannel -import deckers.thibault.aves.MainActivity.Companion.OPEN_FROM_ANALYSIS_SERVICE -import deckers.thibault.aves.channel.calls.* -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.embedding.engine.FlutterEngine -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import kotlinx.coroutines.runBlocking - -class AnalysisService : Service() { - private var flutterEngine: FlutterEngine? = null - private var backgroundChannel: MethodChannel? = null - private var serviceLooper: Looper? = null - private var serviceHandler: ServiceHandler? = null - private val analysisServiceBinder = AnalysisServiceBinder() - - override fun onCreate() { - Log.i(LOG_TAG, "Create analysis service") - runBlocking { - FlutterUtils.initFlutterEngine(this@AnalysisService, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) { - flutterEngine = it - } - } - - try { - initChannels(this) - - HandlerThread("Analysis service handler", Process.THREAD_PRIORITY_BACKGROUND).apply { - start() - serviceLooper = looper - serviceHandler = ServiceHandler(looper) - } - } catch (e: Exception) { - Log.e(LOG_TAG, "failed to initialize service", e) - } - } - - override fun onDestroy() { - Log.i(LOG_TAG, "Destroy analysis service") - } - - override fun onBind(intent: Intent) = analysisServiceBinder - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - val channel = NotificationChannelCompat.Builder(CHANNEL_ANALYSIS, NotificationManagerCompat.IMPORTANCE_LOW) - .setName(getText(R.string.analysis_channel_name)) - .setShowBadge(false) - .build() - NotificationManagerCompat.from(this).createNotificationChannel(channel) - startForeground(NOTIFICATION_ID, buildNotification()) - - val msgData = Bundle() - intent?.extras?.let { - msgData.putAll(it) - } - serviceHandler?.obtainMessage()?.let { msg -> - msg.arg1 = startId - msg.data = msgData - serviceHandler?.sendMessage(msg) - } - - return START_NOT_STICKY - } - - private fun detachAndStop() { - analysisServiceBinder.detach() - stopSelf() - } - - private fun initChannels(context: Context) { - val engine = flutterEngine - engine ?: throw Exception("Flutter engine is not initialized") - - val messenger = engine.dartExecutor - - // channels for analysis - - // dart -> platform -> dart - // - need Context - MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context)) - MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(context)) - MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context)) - MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(context)) - MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(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) } - - // channel for service management - backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply { - setMethodCallHandler { call, result -> onMethodCall(call, result) } - } - } - - private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method) { - "initialized" -> { - Log.d(LOG_TAG, "background channel is ready") - result.success(null) - } - "updateNotification" -> { - val title = call.argument("title") - val message = call.argument("message") - val notification = buildNotification(title, message) - NotificationManagerCompat.from(this).notify(NOTIFICATION_ID, notification) - result.success(null) - } - "refreshApp" -> { - analysisServiceBinder.refreshApp() - result.success(null) - } - "stop" -> { - detachAndStop() - result.success(null) - } - else -> result.notImplemented() - } - } - - private fun buildNotification(title: String? = null, message: String? = null): Notification { - val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - } else { - PendingIntent.FLAG_UPDATE_CURRENT - } - val stopServiceIntent = Intent(this, AnalysisService::class.java).let { - it.putExtra(KEY_COMMAND, COMMAND_STOP) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - PendingIntent.getForegroundService(this, STOP_SERVICE_REQUEST, it, pendingIntentFlags) - } else { - PendingIntent.getService(this, STOP_SERVICE_REQUEST, it, pendingIntentFlags) - } - } - val openAppIntent = Intent(this, MainActivity::class.java).let { - PendingIntent.getActivity(this, OPEN_FROM_ANALYSIS_SERVICE, it, pendingIntentFlags) - } - val stopAction = NotificationCompat.Action.Builder( - R.drawable.ic_outline_stop_24, - getString(R.string.analysis_notification_action_stop), - stopServiceIntent - ).build() - val icon = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) R.drawable.ic_notification else R.mipmap.ic_launcher_round - return NotificationCompat.Builder(this, CHANNEL_ANALYSIS) - .setContentTitle(title ?: getText(R.string.analysis_notification_default_title)) - .setContentText(message) - .setBadgeIconType(NotificationCompat.BADGE_ICON_NONE) - .setSmallIcon(icon) - .setContentIntent(openAppIntent) - .setPriority(NotificationCompat.PRIORITY_LOW) - .addAction(stopAction) - .build() - } - - private inner class ServiceHandler(looper: Looper) : Handler(looper) { - override fun handleMessage(msg: Message) { - val data = msg.data - when (data.getString(KEY_COMMAND)) { - COMMAND_START -> { - runBlocking { - FlutterUtils.runOnUiThread { - val entryIds = data.getIntArray(KEY_ENTRY_IDS)?.toList() - backgroundChannel?.invokeMethod( - "start", hashMapOf( - "entryIds" to entryIds, - "force" to data.getBoolean(KEY_FORCE), - ) - ) - } - } - } - COMMAND_STOP -> { - // unconditionally stop the service - runBlocking { - FlutterUtils.runOnUiThread { - backgroundChannel?.invokeMethod("stop", null) - } - } - detachAndStop() - } - else -> { - } - } - } - } - - companion object { - private val LOG_TAG = LogUtils.createTag() - private const val BACKGROUND_CHANNEL = "deckers.thibault/aves/analysis_service_background" - const val SHARED_PREFERENCES_KEY = "analysis_service" - const val CALLBACK_HANDLE_KEY = "callback_handle" - - const val NOTIFICATION_ID = 1 - const val STOP_SERVICE_REQUEST = 1 - const val CHANNEL_ANALYSIS = "analysis" - - const val KEY_COMMAND = "command" - const val COMMAND_START = "start" - const val COMMAND_STOP = "stop" - const val KEY_ENTRY_IDS = "entry_ids" - const val KEY_FORCE = "force" - } -} - -class AnalysisServiceBinder : Binder() { - private val listeners = hashSetOf() - - fun startListening(listener: AnalysisServiceListener) = listeners.add(listener) - - fun stopListening(listener: AnalysisServiceListener) = listeners.remove(listener) - - fun refreshApp() { - val localListeners = listeners.toSet() - for (listener in localListeners) { - try { - listener.refreshApp() - } catch (e: Exception) { - Log.e(LOG_TAG, "failed to notify listener=$listener", e) - } - } - } - - fun detach() { - val localListeners = listeners.toSet() - for (listener in localListeners) { - try { - listener.detachFromActivity() - } catch (e: Exception) { - Log.e(LOG_TAG, "failed to detach listener=$listener", e) - } - } - } - - companion object { - private val LOG_TAG = LogUtils.createTag() - } -} - -interface AnalysisServiceListener { - fun refreshApp() - fun detachFromActivity() -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisWorker.kt b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisWorker.kt new file mode 100644 index 000000000..16e47dd16 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisWorker.kt @@ -0,0 +1,177 @@ +package deckers.thibault.aves + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.work.CoroutineWorker +import androidx.work.ForegroundInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import app.loup.streams_channel.StreamsChannel +import deckers.thibault.aves.channel.calls.DeviceHandler +import deckers.thibault.aves.channel.calls.GeocodingHandler +import deckers.thibault.aves.channel.calls.MediaStoreHandler +import deckers.thibault.aves.channel.calls.MetadataFetchHandler +import deckers.thibault.aves.channel.calls.StorageHandler +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.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.runBlocking +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class AnalysisWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) { + private var workCont: Continuation? = null + private var flutterEngine: FlutterEngine? = null + private var backgroundChannel: MethodChannel? = null + + override suspend fun doWork(): Result { + createNotificationChannel() + setForeground(createForegroundInfo()) + suspendCoroutine { cont -> + workCont = cont + onStart() + } + return Result.success() + } + + private fun onStart() { + Log.i(LOG_TAG, "Start analysis worker") + runBlocking { + FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) { + flutterEngine = it + } + } + + try { + initChannels(applicationContext) + + runBlocking { + FlutterUtils.runOnUiThread { + backgroundChannel?.invokeMethod( + "start", hashMapOf( + "entryIds" to inputData.getIntArray(KEY_ENTRY_IDS)?.toList(), + "force" to inputData.getBoolean(KEY_FORCE, false), + "progressTotal" to inputData.getInt(KEY_PROGRESS_TOTAL, 0), + "progressOffset" to inputData.getInt(KEY_PROGRESS_OFFSET, 0), + ) + ) + } + } + } catch (e: Exception) { + Log.e(LOG_TAG, "failed to initialize worker", e) + workCont?.resumeWithException(e) + } + } + + private fun initChannels(context: Context) { + val engine = flutterEngine + engine ?: throw Exception("Flutter engine is not initialized") + + val messenger = engine.dartExecutor + + // channels for analysis + + // dart -> platform -> dart + // - need Context + MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context)) + MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(context)) + MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context)) + MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(context)) + MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(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) } + + // channel for service management + backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply { + setMethodCallHandler { call, result -> onMethodCall(call, result) } + } + } + + private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "initialized" -> { + Log.d(LOG_TAG, "Analysis background channel is ready") + result.success(null) + } + + "updateNotification" -> { + val title = call.argument("title") + val message = call.argument("message") + setForegroundAsync(createForegroundInfo(title, message)) + result.success(null) + } + + "stop" -> { + workCont?.resume(null) + result.success(null) + } + + else -> result.notImplemented() + } + } + + private fun createNotificationChannel() { + val channel = NotificationChannelCompat.Builder(NOTIFICATION_CHANNEL, NotificationManagerCompat.IMPORTANCE_LOW) + .setName(applicationContext.getText(R.string.analysis_channel_name)) + .setShowBadge(false) + .build() + NotificationManagerCompat.from(applicationContext).createNotificationChannel(channel) + } + + private fun createForegroundInfo(title: String? = null, message: String? = null): ForegroundInfo { + val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + val openAppIntent = Intent(applicationContext, MainActivity::class.java).let { + PendingIntent.getActivity(applicationContext, MainActivity.OPEN_FROM_ANALYSIS_SERVICE, it, pendingIntentFlags) + } + val stopAction = NotificationCompat.Action.Builder( + R.drawable.ic_outline_stop_24, + applicationContext.getString(R.string.analysis_notification_action_stop), + WorkManager.getInstance(applicationContext).createCancelPendingIntent(id) + ).build() + val icon = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) R.drawable.ic_notification else R.mipmap.ic_launcher_round + val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL) + .setContentTitle(title ?: applicationContext.getText(R.string.analysis_notification_default_title)) + .setContentText(message) + .setBadgeIconType(NotificationCompat.BADGE_ICON_NONE) + .setSmallIcon(icon) + .setContentIntent(openAppIntent) + .setPriority(NotificationCompat.PRIORITY_LOW) + .addAction(stopAction) + .build() + return ForegroundInfo(NOTIFICATION_ID, notification) + } + + companion object { + private val LOG_TAG = LogUtils.createTag() + private const val BACKGROUND_CHANNEL = "deckers.thibault/aves/analysis_service_background" + const val SHARED_PREFERENCES_KEY = "analysis_service" + const val CALLBACK_HANDLE_KEY = "callback_handle" + + const val NOTIFICATION_CHANNEL = "analysis" + const val NOTIFICATION_ID = 1 + + const val KEY_ENTRY_IDS = "entry_ids" + const val KEY_FORCE = "force" + const val KEY_PROGRESS_TOTAL = "progress_total" + const val KEY_PROGRESS_OFFSET = "progress_offset" + } +} diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt index 20961ea40..c65609042 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt @@ -18,6 +18,7 @@ import deckers.thibault.aves.channel.AvesByteSendingMethodCodec import deckers.thibault.aves.channel.calls.* import deckers.thibault.aves.channel.streams.ImageByteStreamHandler import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.FlutterUtils import deckers.thibault.aves.utils.LogUtils import io.flutter.FlutterInjector @@ -40,11 +41,11 @@ class HomeWidgetProvider : AppWidgetProvider() { val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId) defaultScope.launch { - val backgroundBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = false) - updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, backgroundBytes) + val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false) + updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, backgroundProps) - val imageBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false) - updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageBytes) + val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false) + updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageProps) } } } @@ -59,8 +60,8 @@ class HomeWidgetProvider : AppWidgetProvider() { } imageByteFetchJob = defaultScope.launch { delay(500) - val imageBytes = getBytes(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = true) - updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageBytes) + val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = true) + updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageProps) } } @@ -76,13 +77,13 @@ class HomeWidgetProvider : AppWidgetProvider() { return Pair(widthPx, heightPx) } - private suspend fun getBytes( + private suspend fun getProps( context: Context, widgetId: Int, widgetInfo: Bundle, drawEntryImage: Boolean, reuseEntry: Boolean = false, - ): ByteArray? { + ): FieldMap? { val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo) if (widthPx == 0 || heightPx == 0) return null @@ -90,7 +91,7 @@ class HomeWidgetProvider : AppWidgetProvider() { val messenger = flutterEngine!!.dartExecutor val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL) try { - val bytes = suspendCoroutine { cont -> + val props = suspendCoroutine { cont -> defaultScope.launch { FlutterUtils.runOnUiThread { channel.invokeMethod("drawWidget", hashMapOf( @@ -116,7 +117,8 @@ class HomeWidgetProvider : AppWidgetProvider() { } } } - if (bytes is ByteArray) return bytes + @Suppress("unchecked_cast") + return props as FieldMap? } catch (e: Exception) { Log.e(LOG_TAG, "failed to draw widget for widgetId=$widgetId widthPx=$widthPx heightPx=$heightPx", e) } @@ -128,9 +130,16 @@ class HomeWidgetProvider : AppWidgetProvider() { appWidgetManager: AppWidgetManager, widgetId: Int, widgetInfo: Bundle, - bytes: ByteArray?, + props: FieldMap?, ) { - bytes ?: return + props ?: return + + val bytes = props["bytes"] as ByteArray? + val updateOnTap = props["updateOnTap"] as Boolean? + if (bytes == null || updateOnTap == null) { + Log.e(LOG_TAG, "missing arguments") + return + } val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo) if (widthPx == 0 || heightPx == 0) return @@ -139,24 +148,11 @@ class HomeWidgetProvider : AppWidgetProvider() { 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 pendingIntent = if (updateOnTap) buildUpdateIntent(context, widgetId) else buildOpenAppIntent(context, widgetId) val views = RemoteViews(context.packageName, R.layout.app_widget).apply { setImageViewBitmap(R.id.widget_img, bitmap) - setOnClickPendingIntent(R.id.widget_img, activity) + setOnClickPendingIntent(R.id.widget_img, pendingIntent) } appWidgetManager.updateAppWidget(widgetId, views) @@ -166,6 +162,39 @@ class HomeWidgetProvider : AppWidgetProvider() { } } + private fun buildUpdateIntent(context: Context, widgetId: Int): PendingIntent { + val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, Uri.parse("widget://$widgetId"), context, HomeWidgetProvider::class.java) + .putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(widgetId)) + + return PendingIntent.getBroadcast( + 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 + } + ) + } + + private fun buildOpenAppIntent(context: Context, widgetId: Int): PendingIntent { + // 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) + + return 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 + } + ) + } + companion object { private val LOG_TAG = LogUtils.createTag() private const val WIDGET_DART_ENTRYPOINT = "widgetMain" diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt index b9b327b9a..abab47814 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -142,6 +142,7 @@ open class MainActivity : FlutterFragmentActivity() { result.success(intentDataMap) intentDataMap.clear() } + "submitPickedItems" -> submitPickedItems(call) "submitPickedCollectionFilters" -> submitPickedCollectionFilters(call) } @@ -169,7 +170,6 @@ open class MainActivity : FlutterFragmentActivity() { override fun onStop() { Log.i(LOG_TAG, "onStop") - analysisHandler.detachFromActivity() super.onStop() } @@ -204,8 +204,10 @@ open class MainActivity : FlutterFragmentActivity() { DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(requestCode, resultCode, data) DELETE_SINGLE_PERMISSION_REQUEST, MEDIA_WRITE_BULK_PERMISSION_REQUEST -> onScopedStoragePermissionResult(resultCode) + CREATE_FILE_REQUEST, OPEN_FILE_REQUEST -> onStorageAccessResult(requestCode, data?.data) + PICK_COLLECTION_FILTERS_REQUEST -> onCollectionFiltersPickResult(resultCode, data) } } @@ -222,19 +224,17 @@ open class MainActivity : FlutterFragmentActivity() { return } - @SuppressLint("WrongConstant", "ObsoleteSdkInt") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - val canPersist = (intent.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0 - if (canPersist) { - // save access permissions across reboots - val takeFlags = (intent.flags - and (Intent.FLAG_GRANT_READ_URI_PERMISSION - or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) - try { - contentResolver.takePersistableUriPermission(treeUri, takeFlags) - } catch (e: SecurityException) { - Log.w(LOG_TAG, "failed to take persistable URI permission for uri=$treeUri", e) - } + val canPersist = (intent.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0 + @SuppressLint("WrongConstant") + if (canPersist) { + // save access permissions across reboots + val takeFlags = (intent.flags + and (Intent.FLAG_GRANT_READ_URI_PERMISSION + or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) + try { + contentResolver.takePersistableUriPermission(treeUri, takeFlags) + } catch (e: SecurityException) { + Log.w(LOG_TAG, "failed to take persistable URI permission for uri=$treeUri", e) } } @@ -264,6 +264,7 @@ open class MainActivity : FlutterFragmentActivity() { ) } } + Intent.ACTION_VIEW, Intent.ACTION_SEND, "com.android.camera.action.REVIEW" -> { (intent.data ?: intent.getParcelableExtraCompat(Intent.EXTRA_STREAM))?.let { uri -> // MIME type is optional @@ -275,6 +276,19 @@ open class MainActivity : FlutterFragmentActivity() { ) } } + + Intent.ACTION_EDIT -> { + (intent.data ?: intent.getParcelableExtraCompat(Intent.EXTRA_STREAM))?.let { uri -> + // MIME type is optional + val type = intent.type ?: intent.resolveType(this) + return hashMapOf( + INTENT_DATA_KEY_ACTION to INTENT_ACTION_EDIT, + INTENT_DATA_KEY_MIME_TYPE to type, + INTENT_DATA_KEY_URI to uri.toString(), + ) + } + } + Intent.ACTION_GET_CONTENT, Intent.ACTION_PICK -> { return hashMapOf( INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK_ITEMS, @@ -282,6 +296,7 @@ open class MainActivity : FlutterFragmentActivity() { INTENT_DATA_KEY_ALLOW_MULTIPLE to (intent.extras?.getBoolean(Intent.EXTRA_ALLOW_MULTIPLE) ?: false), ) } + Intent.ACTION_SEARCH -> { val viewUri = intent.dataString return if (viewUri != null) hashMapOf( @@ -293,6 +308,7 @@ open class MainActivity : FlutterFragmentActivity() { INTENT_DATA_KEY_QUERY to intent.getStringExtra(SearchManager.QUERY), ) } + INTENT_ACTION_PICK_COLLECTION_FILTERS -> { val initialFilters = extractFiltersFromIntent(intent) return hashMapOf( @@ -300,6 +316,7 @@ open class MainActivity : FlutterFragmentActivity() { 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) { @@ -309,9 +326,11 @@ open class MainActivity : FlutterFragmentActivity() { ) } } + Intent.ACTION_RUN -> { // flutter run } + else -> { Log.w(LOG_TAG, "unhandled intent action=${intent?.action}") } @@ -426,6 +445,7 @@ open class MainActivity : FlutterFragmentActivity() { const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 6 const val PICK_COLLECTION_FILTERS_REQUEST = 7 + const val INTENT_ACTION_EDIT = "edit" 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" diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt index d44a5ed29..248fdcce1 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt @@ -1,6 +1,5 @@ package deckers.thibault.aves.channel.calls -import android.annotation.SuppressLint import android.content.Context import android.content.ContextWrapper import android.content.res.Configuration @@ -27,13 +26,10 @@ class AccessibilityHandler(private val contextWrapper: ContextWrapper) : MethodC private fun areAnimationsRemoved(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { var removed = false - @SuppressLint("ObsoleteSdkInt") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - try { - removed = Settings.Global.getFloat(contextWrapper.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f - } catch (e: Exception) { - Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null) - } + try { + removed = Settings.Global.getFloat(contextWrapper.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null) } result.success(removed) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt index f0f020287..c640d31c7 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AnalysisHandler.kt @@ -1,38 +1,35 @@ package deckers.thibault.aves.channel.calls -import android.annotation.SuppressLint -import android.app.Activity -import android.content.ComponentName import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.Build -import android.os.IBinder -import android.util.Log -import deckers.thibault.aves.AnalysisService -import deckers.thibault.aves.AnalysisServiceBinder -import deckers.thibault.aves.AnalysisServiceListener -import deckers.thibault.aves.utils.ContextUtils.isMyServiceRunning -import deckers.thibault.aves.utils.LogUtils +import androidx.core.app.ComponentActivity +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.workDataOf +import deckers.thibault.aves.AnalysisWorker +import deckers.thibault.aves.utils.FlutterUtils import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking -class AnalysisHandler(private val activity: Activity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler, AnalysisServiceListener { + +class AnalysisHandler(private val activity: ComponentActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler { private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "registerCallback" -> ioScope.launch { Coresult.safe(call, result, ::registerCallback) } - "startService" -> Coresult.safe(call, result, ::startAnalysis) + "startAnalysis" -> Coresult.safe(call, result, ::startAnalysis) else -> result.notImplemented() } } - @SuppressLint("CommitPrefEdits") private fun registerCallback(call: MethodCall, result: MethodChannel.Result) { val callbackHandle = call.argument("callbackHandle")?.toLong() if (callbackHandle == null) { @@ -40,9 +37,9 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp return } - activity.getSharedPreferences(AnalysisService.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) + activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) .edit() - .putLong(AnalysisService.CALLBACK_HANDLE_KEY, callbackHandle) + .putLong(AnalysisWorker.CALLBACK_HANDLE_KEY, callbackHandle) .apply() result.success(true) } @@ -55,22 +52,35 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp } // can be null or empty - val entryIds = call.argument>("entryIds") + val allEntryIds = call.argument>("entryIds") + val progressTotal = allEntryIds?.size ?: 0 + var progressOffset = 0 - if (!activity.isMyServiceRunning(AnalysisService::class.java)) { - val intent = Intent(activity, AnalysisService::class.java) - .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) { - // Foreground services cannot start from background, but the service here may start fine - // while the current lifecycle state (via `ProcessLifecycleOwner.get().lifecycle.currentState`) - // is only `INITIALIZED`, so we should not preemptively return when the state is below `STARTED`. - activity.startForegroundService(intent) - } else { - activity.startService(intent) - } + // work `Data` cannot occupy more than 10240 bytes when serialized + // so we split it when we have a long list of entry IDs + val chunked = allEntryIds?.chunked(WORK_DATA_CHUNK_SIZE) ?: listOf(null) + + fun buildRequest(entryIds: List?, progressOffset: Int): OneTimeWorkRequest { + val workData = workDataOf( + AnalysisWorker.KEY_ENTRY_IDS to entryIds?.toIntArray(), + AnalysisWorker.KEY_FORCE to force, + AnalysisWorker.KEY_PROGRESS_TOTAL to progressTotal, + AnalysisWorker.KEY_PROGRESS_OFFSET to progressOffset, + ) + return OneTimeWorkRequestBuilder().apply { setInputData(workData) }.build() } + + var work = WorkManager.getInstance(activity).beginUniqueWork( + ANALYSIS_WORK_NAME, + ExistingWorkPolicy.KEEP, + buildRequest(chunked.first(), progressOffset), + ) + chunked.drop(1).forEach { entryIds -> + progressOffset += WORK_DATA_CHUNK_SIZE + work = work.then(buildRequest(entryIds, progressOffset)) + } + work.enqueue() + attachToActivity() result.success(null) } @@ -78,44 +88,23 @@ class AnalysisHandler(private val activity: Activity, private val onAnalysisComp private var attached = false fun attachToActivity() { - if (activity.isMyServiceRunning(AnalysisService::class.java)) { - val intent = Intent(activity, AnalysisService::class.java) - activity.bindService(intent, connection, Context.BIND_AUTO_CREATE) + if (!attached) { attached = true - } - } - - override fun detachFromActivity() { - if (attached) { - attached = false - activity.unbindService(connection) - } - } - - override fun refreshApp() { - if (attached) { - onAnalysisCompleted() - } - } - - private val connection = object : ServiceConnection { - var binder: AnalysisServiceBinder? = null - - override fun onServiceConnected(name: ComponentName, service: IBinder) { - Log.i(LOG_TAG, "Analysis service connected") - binder = service as AnalysisServiceBinder - binder?.startListening(this@AnalysisHandler) - } - - override fun onServiceDisconnected(name: ComponentName) { - Log.i(LOG_TAG, "Analysis service disconnected") - binder?.stopListening(this@AnalysisHandler) - binder = null + WorkManager.getInstance(activity).getWorkInfosForUniqueWorkLiveData(ANALYSIS_WORK_NAME).observe(activity) { list -> + if (list.any { it.state == WorkInfo.State.SUCCEEDED }) { + runBlocking { + FlutterUtils.runOnUiThread { + onAnalysisCompleted() + } + } + } + } } } companion object { - private val LOG_TAG = LogUtils.createTag() const val CHANNEL = "deckers.thibault/aves/analysis" + private const val ANALYSIS_WORK_NAME = "analysis_work" + private const val WORK_DATA_CHUNK_SIZE = 1000 } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt index 18921bfe6..2ec3c761e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -1,6 +1,5 @@ package deckers.thibault.aves.channel.calls -import android.annotation.SuppressLint import android.content.* import android.content.pm.ApplicationInfo import android.content.res.Configuration @@ -40,6 +39,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File import java.util.* import kotlin.math.roundToInt @@ -69,13 +69,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { // apps tend to use their name in English when creating directories // so we get their names in English as well as the current locale val englishConfig = Configuration().apply { - @SuppressLint("ObsoleteSdkInt") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - setLocale(Locale.ENGLISH) - } else { - @Suppress("deprecation") - locale = Locale.ENGLISH - } + setLocale(Locale.ENGLISH) } val pm = context.packageManager @@ -169,8 +163,8 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { .submit(size, size) try { - @Suppress("BlockingMethodInNonBlockingContext") - data = target.get()?.getBytes(canHaveAlpha = true, recycle = false) + val bitmap = withContext(Dispatchers.IO) { target.get() } + data = bitmap?.getBytes(canHaveAlpha = true, recycle = false) } catch (e: Exception) { Log.w(LOG_TAG, "failed to decode app icon for packageName=$packageName", e) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt index 8a6a0b8d3..30348d57b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt @@ -40,7 +40,6 @@ class DeviceHandler(private val context: Context) : MethodCallHandler { hashMapOf( "canGrantDirectoryAccess" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP), "canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context), - "canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT), "canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.M), "canRenderSubdivisionFlagEmojis" to (sdkInt >= Build.VERSION_CODES.O), "canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S), diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt index d42e5ea50..028d4fec3 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt @@ -64,7 +64,6 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { if (canReadWithExifInterface(mimeType)) { try { Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> - @Suppress("BlockingMethodInNonBlockingContext") val exif = ExifInterface(input) val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) exif.thumbnailBitmap?.let { bitmap -> diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt index 7fac5c664..6af0642e2 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GeocodingHandler.kt @@ -3,8 +3,8 @@ package deckers.thibault.aves.channel.calls import android.content.Context import android.location.Address import android.location.Geocoder -import android.os.Build import deckers.thibault.aves.channel.calls.Coresult.Companion.safe +import deckers.thibault.aves.utils.getFromLocationCompat import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -12,8 +12,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import java.io.IOException -import java.util.* +import java.util.Locale // as of 2021/03/10, geocoding packages exist but: // - `geocoder` is unmaintained @@ -76,26 +75,9 @@ class GeocodingHandler(private val context: Context) : MethodCallHandler { } } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - geocoder!!.getFromLocation(latitude, longitude, maxResults, object : Geocoder.GeocodeListener { - override fun onGeocode(addresses: List) = processAddresses(addresses.filterNotNull()) - - override fun onError(errorMessage: String?) { - result.error("getAddress-asyncerror", "failed to get address", errorMessage) - } - }) - } else { - try { - @Suppress("deprecation") - val addresses = geocoder!!.getFromLocation(latitude, longitude, maxResults) ?: ArrayList() - processAddresses(addresses) - } catch (e: IOException) { - // `grpc failed`, etc. - result.error("getAddress-network", "failed to get address because of network issues", e.message) - } catch (e: Exception) { - result.error("getAddress-exception", "failed to get address", e.message) - } - } + geocoder!!.getFromLocationCompat( + latitude, longitude, maxResults, ::processAddresses, + ) { code, message, details -> result.error(code, message, details) } } companion object { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt index d0d7767e9..c614ab6ea 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/GlobalSearchHandler.kt @@ -1,13 +1,15 @@ package deckers.thibault.aves.channel.calls -import android.annotation.SuppressLint import android.content.Context import deckers.thibault.aves.SearchSuggestionsProvider import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch class GlobalSearchHandler(private val context: Context) : MethodCallHandler { private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) @@ -19,7 +21,6 @@ class GlobalSearchHandler(private val context: Context) : MethodCallHandler { } } - @SuppressLint("CommitPrefEdits") private fun registerCallback(call: MethodCall, result: MethodChannel.Result) { val callbackHandle = call.argument("callbackHandle")?.toLong() if (callbackHandle == null) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt index feda79f93..e04dc7241 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt @@ -1,6 +1,5 @@ package deckers.thibault.aves.channel.calls -import android.annotation.SuppressLint import android.content.Context import android.media.MediaMetadataRetriever import android.net.Uri @@ -28,20 +27,30 @@ import com.drew.metadata.png.PngDirectory import com.drew.metadata.webp.WebpDirectory import com.drew.metadata.xmp.XmpDirectory import deckers.thibault.aves.channel.calls.Coresult.Companion.safe -import deckers.thibault.aves.metadata.* +import deckers.thibault.aves.metadata.ExifGeoTiffTags import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDouble import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeInt import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeRational +import deckers.thibault.aves.metadata.ExifTags +import deckers.thibault.aves.metadata.GSpherical +import deckers.thibault.aves.metadata.GeoTiffKeys +import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDateMillis import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDescription import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeInt +import deckers.thibault.aves.metadata.Metadata import deckers.thibault.aves.metadata.Metadata.DIR_DNG import deckers.thibault.aves.metadata.Metadata.DIR_EXIF_GEOTIFF import deckers.thibault.aves.metadata.Metadata.DIR_PNG_TEXTUAL_DATA import deckers.thibault.aves.metadata.Metadata.getRotationDegreesForExifCode import deckers.thibault.aves.metadata.Metadata.isFlippedForExifCode +import deckers.thibault.aves.metadata.Mp4ParserHelper +import deckers.thibault.aves.metadata.MultiPage +import deckers.thibault.aves.metadata.PixyMetaHelper +import deckers.thibault.aves.metadata.QuickTimeMetadata +import deckers.thibault.aves.metadata.XMP import deckers.thibault.aves.metadata.XMP.doesPropExist import deckers.thibault.aves.metadata.XMP.getPropArrayItemValues import deckers.thibault.aves.metadata.XMP.getSafeDateMillis @@ -66,6 +75,7 @@ import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeInt import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeRational import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeString import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir +import deckers.thibault.aves.metadata.metadataextractor.PngActlDirectory import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue import deckers.thibault.aves.utils.LogUtils @@ -84,7 +94,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import org.json.JSONObject -import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.text.DecimalFormat import java.text.ParseException @@ -305,12 +314,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { val key = kv.key // `PNG-iTXt` uses UTF-8, contrary to `PNG-tEXt` and `PNG-zTXt` using Latin-1 / ISO-8859-1 val charset = if (baseDirName == PNG_ITXT_DIR_NAME) { - @SuppressLint("ObsoleteSdkInt") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - StandardCharsets.UTF_8 - } else { - Charset.forName("UTF-8") - } + StandardCharsets.UTF_8 } else { kv.value.charset } @@ -654,6 +658,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { } } } + + // identification of animated PNG + if (metadata.containsDirectoryOfType(PngActlDirectory::class.java)) { + flags = flags or MASK_IS_ANIMATED + } } MimeTypes.GIF -> { @@ -747,10 +756,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { var flags = (metadataMap[KEY_FLAGS] ?: 0) as Int try { - @SuppressLint("ObsoleteSdkInt") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { metadataMap[KEY_ROTATION_DEGREES] = it } - } + retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { metadataMap[KEY_ROTATION_DEGREES] = it } if (!metadataMap.containsKey(KEY_DATE_MILLIS)) { retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { metadataMap[KEY_DATE_MILLIS] = it } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt index a022bfd8d..60276c136 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt @@ -6,12 +6,12 @@ import android.graphics.BitmapFactory import android.graphics.BitmapRegionDecoder import android.graphics.Rect import android.net.Uri -import android.os.Build import com.bumptech.glide.Glide import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions import deckers.thibault.aves.decoder.MultiTrackImage +import deckers.thibault.aves.utils.BitmapRegionDecoderCompat import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils @@ -68,13 +68,7 @@ class RegionFetcher internal constructor( try { if (currentDecoderRef == null) { val newDecoder = StorageUtils.openInputStream(context, uri)?.use { input -> - @Suppress("BlockingMethodInNonBlockingContext") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - BitmapRegionDecoder.newInstance(input) - } else { - @Suppress("deprecation") - BitmapRegionDecoder.newInstance(input, false) - } + BitmapRegionDecoderCompat.newInstance(input) } if (newDecoder == null) { result.error("getRegion-read-null", "failed to open file for mimeType=$mimeType uri=$uri regionRect=$regionRect", null) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt index 1f6590dd6..bf4c37bcd 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt @@ -8,7 +8,7 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel class ActivityWindowHandler(private val activity: Activity) : WindowHandler(activity) { - override fun isActivity(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + override fun isActivity(call: MethodCall, result: MethodChannel.Result) { result.success(true) } @@ -49,11 +49,11 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti result.success(true) } - override fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + override fun isCutoutAware(call: MethodCall, result: MethodChannel.Result) { result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) } - override fun getCutoutInsets(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + override fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { result.error("getCutoutInsets-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null) return diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt index 3a50fd324..1332bbb9a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt @@ -5,7 +5,7 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel class ServiceWindowHandler(service: Service) : WindowHandler(service) { - override fun isActivity(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + override fun isActivity(call: MethodCall, result: MethodChannel.Result) { result.success(false) } @@ -21,7 +21,7 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) { result.success(false) } - override fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + override fun isCutoutAware(call: MethodCall, result: MethodChannel.Result) { result.success(false) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt index 492c6deeb..11472d6a5 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt @@ -40,7 +40,7 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho abstract fun requestOrientation(call: MethodCall, result: MethodChannel.Result) - abstract fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) + abstract fun isCutoutAware(call: MethodCall, result: MethodChannel.Result) abstract fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt index c5f319dd2..e9301a1f9 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt @@ -1,6 +1,5 @@ package deckers.thibault.aves.channel.streams -import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.net.Uri @@ -77,7 +76,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any private fun requestMediaFileAccess() { val uris = (args["uris"] as List<*>?)?.mapNotNull { if (it is String) Uri.parse(it) else null } val mimeTypes = (args["mimeTypes"] as List<*>?)?.mapNotNull { if (it is String) it else null } - if (uris == null || uris.isEmpty() || mimeTypes == null || mimeTypes.size != uris.size) { + if (uris.isNullOrEmpty() || mimeTypes == null || mimeTypes.size != uris.size) { error("requestMediaFileAccess-args", "missing arguments", null) return } @@ -112,12 +111,6 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any } private suspend fun createFile() { - @SuppressLint("ObsoleteSdkInt") - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - error("createFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null) - return - } - val name = args["name"] as String? val mimeType = args["mimeType"] as String? val bytes = args["bytes"] as ByteArray? @@ -155,12 +148,6 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any } private suspend fun openFile() { - @SuppressLint("ObsoleteSdkInt") - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - error("openFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null) - return - } - val mimeType = args["mimeType"] as String? // optional fun onGranted(uri: Uri) { @@ -219,7 +206,6 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any } } - @Suppress("SameParameterValue") private fun error(errorCode: String, errorMessage: String, errorDetails: Any?) { handler.post { try { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt index 2cf3654cc..c140996d2 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.InputStream class ImageByteStreamHandler(private val context: Context, private val arguments: Any?) : EventChannel.StreamHandler { @@ -144,8 +145,7 @@ class ImageByteStreamHandler(private val context: Context, private val arguments .load(model) .submit() try { - @Suppress("BlockingMethodInNonBlockingContext") - var bitmap = target.get() + var bitmap = withContext(Dispatchers.IO) { target.get() } if (needRotationAfterGlide(mimeType)) { bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped) } @@ -173,8 +173,7 @@ class ImageByteStreamHandler(private val context: Context, private val arguments .load(VideoThumbnail(context, uri)) .submit() try { - @Suppress("BlockingMethodInNonBlockingContext") - val bitmap = target.get() + val bitmap = withContext(Dispatchers.IO) { target.get() } if (bitmap != null) { val bytes = bitmap.getBytes(canHaveAlpha = false, recycle = false) if (MemoryUtils.canAllocate(sizeBytes)) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt index 77a753bfc..b29b8d4f2 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt @@ -1,10 +1,8 @@ package deckers.thibault.aves.channel.streams -import android.annotation.SuppressLint import android.content.Context import android.database.ContentObserver import android.net.Uri -import android.os.Build import android.os.Handler import android.os.Looper import android.provider.Settings @@ -34,14 +32,12 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S override fun onChange(selfChange: Boolean, uri: Uri?) { if (update()) { - val settings: FieldMap = hashMapOf( - Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation, + success( + hashMapOf( + Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation, + Settings.Global.TRANSITION_ANIMATION_SCALE to transitionAnimationScale, + ) ) - @SuppressLint("ObsoleteSdkInt") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - settings[Settings.Global.TRANSITION_ANIMATION_SCALE] = transitionAnimationScale - } - success(settings) } } @@ -53,13 +49,10 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S accelerometerRotation = newAccelerometerRotation changed = true } - @SuppressLint("ObsoleteSdkInt") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - val newTransitionAnimationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) - if (transitionAnimationScale != newTransitionAnimationScale) { - transitionAnimationScale = newTransitionAnimationScale - changed = true - } + val newTransitionAnimationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) + if (transitionAnimationScale != newTransitionAnimationScale) { + transitionAnimationScale = newTransitionAnimationScale + changed = true } } catch (e: Exception) { Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt index a481c94e4..60b74a507 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt @@ -81,4 +81,4 @@ class GoogleDeviceContainer { fun itemMimeType(index: Int) = item(index)?.mimeType } -class GoogleDeviceContainerItem(val mimeType: String, val length: Long, val dataUri: String) {} +class GoogleDeviceContainerItem(val mimeType: String, val length: Long, val dataUri: String) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt index cfde73855..2c2616177 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MediaMetadataRetrieverHelper.kt @@ -1,11 +1,12 @@ package deckers.thibault.aves.metadata -import android.annotation.SuppressLint import android.media.MediaFormat import android.media.MediaMetadataRetriever import android.os.Build import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale +import java.util.TimeZone object MediaMetadataRetrieverHelper { val allKeys = hashMapOf( @@ -31,11 +32,8 @@ object MediaMetadataRetrieverHelper { MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH to "Video Width", MediaMetadataRetriever.METADATA_KEY_WRITER to "Writer", MediaMetadataRetriever.METADATA_KEY_YEAR to "Year", + MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION to "Video Rotation", ).apply { - @SuppressLint("ObsoleteSdkInt") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - put(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION, "Video Rotation") - } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { put(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE, "Capture Framerate") } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt index 699dc4009..c8522a6cf 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt @@ -134,6 +134,8 @@ object Metadata { private fun getSafeUri(context: Context, uri: Uri, mimeType: String, sizeBytes: Long?): Uri { return when (mimeType) { // formats known to yield OOM for large files + MimeTypes.HEIC, + MimeTypes.HEIF, MimeTypes.MP4, MimeTypes.PSD_VND, MimeTypes.PSD_X, diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt index 1693c4bab..25f1bab85 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt @@ -1,6 +1,5 @@ package deckers.thibault.aves.metadata -import android.annotation.SuppressLint import android.content.Context import android.media.MediaExtractor import android.media.MediaFormat @@ -63,10 +62,7 @@ object MultiPage { format.getSafeInt(MediaFormat.KEY_WIDTH) { track[KEY_WIDTH] = it } format.getSafeInt(MediaFormat.KEY_HEIGHT) { track[KEY_HEIGHT] = it } - @SuppressLint("ObsoleteSdkInt") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { track[KEY_IS_DEFAULT] = it != 0 } - } + format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { track[KEY_IS_DEFAULT] = it != 0 } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { format.getSafeInt(MediaFormat.KEY_ROTATION) { track[KEY_ROTATION_DEGREES] = it } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt index ed247f4cf..2c71d16ed 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt @@ -67,14 +67,16 @@ object Helper { val metadata = when (fileType) { FileType.Jpeg -> safeReadJpeg(inputStream) + FileType.Mp4 -> safeReadMp4(inputStream) FileType.Png -> safeReadPng(inputStream) + FileType.Psd -> safeReadPsd(inputStream) FileType.Tiff, FileType.Arw, FileType.Cr2, FileType.Nef, FileType.Orf, FileType.Rw2 -> safeReadTiff(inputStream) - FileType.Mp4 -> safeReadMp4(inputStream) + else -> ImageMetadataReader.readMetadata(inputStream, safeReadStreamLength, fileType) } @@ -100,6 +102,10 @@ object Helper { return SafePngMetadataReader.readMetadata(input) } + private fun safeReadPsd(input: InputStream): com.drew.metadata.Metadata { + return SafePsdMetadataReader.readMetadata(input) + } + @Throws(IOException::class, TiffProcessingException::class) fun safeReadTiff(input: InputStream): com.drew.metadata.Metadata { val reader = RandomAccessStreamReader(input, RandomAccessStreamReader.DEFAULT_CHUNK_LENGTH, safeReadStreamLength) @@ -262,6 +268,7 @@ object Helper { ExifReader().extract(ByteArrayReader(dataBytes), metadata, ExifReader.JPEG_SEGMENT_PREAMBLE.length) } } + PNG_RAW_PROFILE_IPTC -> { val start = dataBytes.indexOf(Metadata.IPTC_MARKER_BYTE) if (start != -1) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/PngActlDirectory.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/PngActlDirectory.kt new file mode 100644 index 000000000..93138826a --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/PngActlDirectory.kt @@ -0,0 +1,22 @@ +package deckers.thibault.aves.metadata.metadataextractor +import com.drew.imaging.png.PngChunkType +import com.drew.metadata.png.PngDirectory + +class PngActlDirectory : PngDirectory(chunkType) { + override fun getTagNameMap(): HashMap { + return tagNames + } + + companion object { + val chunkType = PngChunkType("acTL") + + // tags should be distinct from those already defined in `PngDirectory` + const val TAG_NUM_FRAMES = 101 + const val TAG_NUM_PLAYS = 102 + + private val tagNames = hashMapOf( + TAG_NUM_FRAMES to "Number Of Frames", + TAG_NUM_PLAYS to "Number Of Plays", + ) + } +} diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeMp4BoxHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeMp4BoxHandler.kt index 2e6bebb3b..b4e83772c 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeMp4BoxHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeMp4BoxHandler.kt @@ -1,8 +1,6 @@ package deckers.thibault.aves.metadata.metadataextractor import com.drew.imaging.mp4.Mp4Handler -import com.drew.lang.annotations.NotNull -import com.drew.lang.annotations.Nullable import com.drew.metadata.Metadata import com.drew.metadata.mp4.Mp4BoxHandler import com.drew.metadata.mp4.Mp4BoxTypes @@ -11,7 +9,7 @@ import java.io.IOException class SafeMp4BoxHandler(metadata: Metadata) : Mp4BoxHandler(metadata) { @Throws(IOException::class) - override fun processBox(@NotNull type: String, @Nullable payload: ByteArray?, boxSize: Long, context: Mp4Context?): Mp4Handler<*>? { + override fun processBox(type: String, payload: ByteArray?, boxSize: Long, context: Mp4Context?): Mp4Handler<*>? { if (payload != null && type == Mp4BoxTypes.BOX_USER_DEFINED) { val userBoxHandler = SafeMp4UuidBoxHandler(metadata) userBoxHandler.processBox(type, payload, boxSize, context) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePhotoshopReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePhotoshopReader.kt new file mode 100644 index 000000000..d312569bd --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePhotoshopReader.kt @@ -0,0 +1,116 @@ +package deckers.thibault.aves.metadata.metadataextractor + +import com.drew.imaging.ImageProcessingException +import com.drew.lang.ByteArrayReader +import com.drew.lang.SequentialByteArrayReader +import com.drew.lang.SequentialReader +import com.drew.metadata.Directory +import com.drew.metadata.Metadata +import com.drew.metadata.exif.ExifReader +import com.drew.metadata.icc.IccReader +import com.drew.metadata.iptc.IptcReader +import com.drew.metadata.photoshop.PhotoshopDirectory +import com.drew.metadata.photoshop.PhotoshopReader +import java.util.Arrays + +// adapted from `PhotoshopReader` to prevent OOM from reading large XMP +// as of `metadata-extractor` v2.18.0, there is no way to customize the Photoshop reader +// without copying the whole `extract` function +class SafePhotoshopReader : PhotoshopReader() { + override fun extract(reader: SequentialReader, length: Int, metadata: Metadata, parentDirectory: Directory?) { + val directory = PhotoshopDirectory() + metadata.addDirectory(directory) + + if (parentDirectory != null) { + directory.parent = parentDirectory + } + + // Data contains a sequence of Image Resource Blocks (IRBs): + // + // 4 bytes - Signature; mostly "8BIM" but "PHUT", "AgHg" and "DCSR" are also found + // 2 bytes - Resource identifier + // String - Pascal string, padded to make length even + // 4 bytes - Size of resource data which follows + // Data - The resource data, padded to make size even + // + // http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037504 + + var pos = 0 + var clippingPathCount = 0 + while (pos < length) { + try { + // 4 bytes for the signature ("8BIM", "PHUT", etc.) + val signature = reader.getString(4) + pos += 4 + + // 2 bytes for the resource identifier (tag type). + val tagType = reader.uInt16 // segment type + pos += 2 + + // A variable number of bytes holding a pascal string (two leading bytes for length). + var descriptionLength = reader.uInt8.toInt() + pos += 1 + // Some basic bounds checking + if (descriptionLength < 0 || descriptionLength + pos > length) { + throw ImageProcessingException("Invalid string length") + } + + // Get name (important for paths) + val description = StringBuilder() + descriptionLength += pos + // Loop through each byte and append to string + while (pos < descriptionLength) { + description.append(Char(reader.uInt8.toUShort())) + pos++ + } + + // The number of bytes is padded with a trailing zero, if needed, to make the size even. + if (pos % 2 != 0) { + reader.skip(1) + pos++ + } + + // 4 bytes for the size of the resource data that follows. + val byteCount = reader.int32 + pos += 4 + // The resource data. + var tagBytes = reader.getBytes(byteCount) + pos += byteCount + // The number of bytes is padded with a trailing zero, if needed, to make the size even. + if (pos % 2 != 0) { + reader.skip(1) + pos++ + } + + if (signature == "8BIM") { + when (tagType) { + PhotoshopDirectory.TAG_IPTC -> IptcReader().extract(SequentialByteArrayReader(tagBytes), metadata, tagBytes.size.toLong(), directory) + PhotoshopDirectory.TAG_ICC_PROFILE_BYTES -> IccReader().extract(ByteArrayReader(tagBytes), metadata, directory) + PhotoshopDirectory.TAG_EXIF_DATA_1, + PhotoshopDirectory.TAG_EXIF_DATA_3 -> ExifReader().extract(ByteArrayReader(tagBytes), metadata, 0, directory) + + PhotoshopDirectory.TAG_XMP_DATA -> SafeXmpReader().extract(tagBytes, metadata, directory) + in 0x07D0..0x0BB6 -> { + clippingPathCount++ + tagBytes = Arrays.copyOf(tagBytes, tagBytes.size + description.length + 1) + // Append description(name) to end of byte array with 1 byte before the description representing the length + for (i in tagBytes.size - description.length - 1 until tagBytes.size) { + if (i % (tagBytes.size - description.length - 1 + description.length) == 0) tagBytes[i] = description.length.toByte() else tagBytes[i] = description[i - (tagBytes.size - description.length - 1)].code.toByte() + } +// PhotoshopDirectory._tagNameMap[0x07CF + clippingPathCount] = "Path Info $clippingPathCount" + directory.setByteArray(0x07CF + clippingPathCount, tagBytes) + } + + else -> directory.setByteArray(tagType, tagBytes) + } +// if (tagType in 0x0fa0..0x1387) { +// PhotoshopDirectory._tagNameMap[tagType] = String.format("Plug-in %d Data", tagType - 0x0fa0 + 1) +// } + } + } catch (ex: Exception) { + directory.addError(ex.message) + return + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt index 1f2cc1dd5..c438068b5 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt @@ -1,11 +1,24 @@ package deckers.thibault.aves.metadata.metadataextractor import android.util.Log -import com.drew.imaging.png.* +import com.drew.imaging.png.PngChromaticities +import com.drew.imaging.png.PngChunk +import com.drew.imaging.png.PngChunkReader +import com.drew.imaging.png.PngChunkType +import com.drew.imaging.png.PngHeader +import com.drew.imaging.png.PngProcessingException import com.drew.imaging.tiff.TiffProcessingException import com.drew.imaging.tiff.TiffReader -import com.drew.lang.* -import com.drew.lang.annotations.NotNull +import com.drew.lang.ByteArrayReader +import com.drew.lang.ByteConvert +import com.drew.lang.Charsets +import com.drew.lang.DateUtil +import com.drew.lang.KeyValuePair +import com.drew.lang.RandomAccessStreamReader +import com.drew.lang.SequentialByteArrayReader +import com.drew.lang.SequentialReader +import com.drew.lang.StreamReader +import com.drew.lang.StreamUtil import com.drew.metadata.ErrorDirectory import com.drew.metadata.Metadata import com.drew.metadata.StringValue @@ -21,9 +34,10 @@ import java.io.InputStream import java.util.zip.InflaterInputStream import java.util.zip.ZipException -// adapted from `PngMetadataReader` to prevent reading OOM from large chunks -// as of `metadata-extractor` v2.18.0, there is no way to customize the reader -// without copying `desiredChunkTypes` and the whole `processChunk` function +// adapted from `PngMetadataReader` to: +// - prevent OOM from reading large chunks. As of `metadata-extractor` v2.18.0, there is no way to customize the reader +// without copying `desiredChunkTypes` and the whole `processChunk` function. +// - parse `acTL` chunk to identify animated PNGs. object SafePngMetadataReader { private val LOG_TAG = LogUtils.createTag() @@ -47,6 +61,7 @@ object SafePngMetadataReader { PngChunkType.pHYs, PngChunkType.sBIT, PngChunkType.eXIf, + PngActlDirectory.chunkType, ) @Throws(IOException::class, PngProcessingException::class) @@ -64,7 +79,7 @@ object SafePngMetadataReader { } @Throws(PngProcessingException::class, IOException::class) - private fun processChunk(@NotNull metadata: Metadata, @NotNull chunk: PngChunk) { + private fun processChunk(metadata: Metadata, chunk: PngChunk) { val chunkType = chunk.type val bytes = chunk.bytes @@ -86,6 +101,21 @@ object SafePngMetadataReader { directory.setInt(PngDirectory.TAG_FILTER_METHOD, header.filterMethod.toInt()) directory.setInt(PngDirectory.TAG_INTERLACE_METHOD, header.interlaceMethod.toInt()) metadata.addDirectory(directory) + // TLAD insert start + } else if (chunkType == PngActlDirectory.chunkType) { + if (bytes.size != 8) { + throw PngProcessingException("Invalid number of bytes") + } + val reader = SequentialByteArrayReader(bytes) + try { + metadata.addDirectory(PngActlDirectory().apply { + setInt(PngActlDirectory.TAG_NUM_FRAMES, reader.int32) + setInt(PngActlDirectory.TAG_NUM_PLAYS, reader.int32) + }) + } catch (ex: IOException) { + throw PngProcessingException(ex) + } + // TLAD insert end } else if (chunkType == PngChunkType.PLTE) { val directory = PngDirectory(PngChunkType.PLTE) directory.setInt(PngDirectory.TAG_PALETTE_SIZE, bytes.size / 3) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdMetadataReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdMetadataReader.kt new file mode 100644 index 000000000..6f93a23f8 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdMetadataReader.kt @@ -0,0 +1,13 @@ +package deckers.thibault.aves.metadata.metadataextractor + +import com.drew.lang.StreamReader +import com.drew.metadata.Metadata +import java.io.InputStream + +object SafePsdMetadataReader { + fun readMetadata(inputStream: InputStream): Metadata { + val metadata = Metadata() + SafePsdReader().extract(StreamReader(inputStream), metadata) + return metadata + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdReader.kt new file mode 100644 index 000000000..91dcf8444 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdReader.kt @@ -0,0 +1,93 @@ +package deckers.thibault.aves.metadata.metadataextractor + +import com.drew.lang.SequentialReader +import com.drew.metadata.Metadata +import com.drew.metadata.photoshop.PsdHeaderDirectory +import java.io.IOException + +// adapted from `PsdReader` to prevent OOM from reading large XMP +// as of `metadata-extractor` v2.18.0, there is no way to customize the Photoshop reader +// without copying the whole `extract` function +class SafePsdReader { + fun extract(reader: SequentialReader, metadata: Metadata) { + val directory = PsdHeaderDirectory() + metadata.addDirectory(directory) + + // FILE HEADER SECTION + + try { + val signature = reader.int32 + if (signature != 0x38425053) // "8BPS" + { + directory.addError("Invalid PSD file signature") + return + } + + val version = reader.uInt16 + if (version != 1 && version != 2) { + directory.addError("Invalid PSD file version (must be 1 or 2)") + return + } + + // 6 reserved bytes are skipped here. They should be zero. + reader.skip(6) + + val channelCount = reader.uInt16 + directory.setInt(PsdHeaderDirectory.TAG_CHANNEL_COUNT, channelCount) + + // even though this is probably an unsigned int, the max height in practice is 300,000 + val imageHeight = reader.int32 + directory.setInt(PsdHeaderDirectory.TAG_IMAGE_HEIGHT, imageHeight) + + // even though this is probably an unsigned int, the max width in practice is 300,000 + val imageWidth = reader.int32 + directory.setInt(PsdHeaderDirectory.TAG_IMAGE_WIDTH, imageWidth) + + val bitsPerChannel = reader.uInt16 + directory.setInt(PsdHeaderDirectory.TAG_BITS_PER_CHANNEL, bitsPerChannel) + + val colorMode = reader.uInt16 + directory.setInt(PsdHeaderDirectory.TAG_COLOR_MODE, colorMode) + } catch (e: IOException) { + directory.addError("Unable to read PSD header") + return + } + + // COLOR MODE DATA SECTION + + try { + val sectionLength = reader.uInt32 + + /* + * Only indexed color and duotone (see the mode field in the File header section) have color mode data. + * For all other modes, this section is just the 4-byte length field, which is set to zero. + * + * Indexed color images: length is 768; color data contains the color table for the image, + * in non-interleaved order. + * Duotone images: color data contains the duotone specification (the format of which is not documented). + * Other applications that read Photoshop files can treat a duotone image as a gray image, + * and just preserve the contents of the duotone information when reading and writing the + * file. + */ + reader.skip(sectionLength) + } catch (e: IOException) { + return + } + + // IMAGE RESOURCES SECTION + + try { + val sectionLength = reader.uInt32 + + assert(sectionLength <= Int.MAX_VALUE) + + SafePhotoshopReader().extract(reader, sectionLength.toInt(), metadata) + } catch (e: IOException) { + // ignore + } + + // LAYER AND MASK INFORMATION SECTION (skipped) + + // IMAGE DATA SECTION (skipped) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt index ac07d9967..e36402907 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt @@ -10,8 +10,6 @@ import com.adobe.internal.xmp.properties.XMPPropertyInfo import com.drew.imaging.jpeg.JpegSegmentType import com.drew.lang.SequentialByteArrayReader import com.drew.lang.SequentialReader -import com.drew.lang.annotations.NotNull -import com.drew.lang.annotations.Nullable import com.drew.metadata.Directory import com.drew.metadata.Metadata import com.drew.metadata.xmp.XmpDirectory @@ -63,12 +61,19 @@ class SafeXmpReader : XmpReader() { } // adapted from `XmpReader` to provide different parsing options - override fun extract(@NotNull xmpBytes: ByteArray, offset: Int, length: Int, @NotNull metadata: Metadata, @Nullable parentDirectory: Directory?) { + // and to detect large XMP when extracted directly (e.g. from Photoshop reader) + override fun extract(xmpBytes: ByteArray, offset: Int, length: Int, metadata: Metadata, parentDirectory: Directory?) { + val totalSize = xmpBytes.size + if (totalSize > SEGMENT_TYPE_SIZE_DANGER_THRESHOLD) { + logError(metadata, totalSize) + return + } + val directory = XmpDirectory() if (parentDirectory != null) directory.parent = parentDirectory try { - val xmpMeta: XMPMeta = if (offset == 0 && length == xmpBytes.size) { + val xmpMeta: XMPMeta = if (offset == 0 && length == totalSize) { XMPMetaFactory.parseFromBuffer(xmpBytes, PARSE_OPTIONS) } else { val buffer = ByteBuffer(xmpBytes, offset, length) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt index e858c03e4..3fbb68b33 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt @@ -1,12 +1,10 @@ package deckers.thibault.aves.model -import android.annotation.SuppressLint import android.content.ContentResolver import android.content.Context import android.graphics.BitmapFactory import android.media.MediaMetadataRetriever import android.net.Uri -import android.os.Build import androidx.exifinterface.media.ExifInterface import com.drew.metadata.avi.AviDirectory import com.drew.metadata.exif.ExifIFD0Directory @@ -147,10 +145,7 @@ class SourceEntry { retriever.getSafeLong(MediaMetadataRetriever.METADATA_KEY_DURATION) { durationMillis = it } retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { sourceDateTakenMillis = it } retriever.getSafeString(MediaMetadataRetriever.METADATA_KEY_TITLE) { title = it } - @SuppressLint("ObsoleteSdkInt") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { sourceRotationDegrees = it } - } + retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { sourceRotationDegrees = it } } catch (e: Exception) { // ignore } finally { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index f6c00b475..ac1624c5b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -45,11 +45,14 @@ import deckers.thibault.aves.utils.MimeTypes.canReadWithPixyMeta import deckers.thibault.aves.utils.MimeTypes.canRemoveMetadata import deckers.thibault.aves.utils.MimeTypes.extensionFor import deckers.thibault.aves.utils.MimeTypes.isVideo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import pixy.meta.meta.Metadata import pixy.meta.meta.MetadataType import java.io.* import java.nio.channels.Channels import java.util.* +import kotlin.math.absoluteValue abstract class ImageProvider { open fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) { @@ -308,8 +311,7 @@ abstract class ImageProvider { .apply(glideOptions) .load(model) .submit(targetWidthPx, targetHeightPx) - @Suppress("BlockingMethodInNonBlockingContext") - var bitmap = target.get() + var bitmap = withContext(Dispatchers.IO) { target.get() } if (MimeTypes.needRotationAfterGlide(sourceMimeType)) { bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped) } @@ -457,7 +459,6 @@ abstract class ImageProvider { editableFile.delete() } - @Suppress("BlockingMethodInNonBlockingContext") suspend fun captureFrame( contextWrapper: ContextWrapper, desiredNameWithoutExtension: String, @@ -512,7 +513,7 @@ abstract class ImageProvider { output.write(bytes) } } else { - val editableFile = File.createTempFile("aves", null).apply { + val editableFile = withContext(Dispatchers.IO) { File.createTempFile("aves", null) }.apply { deleteOnExit() transferFrom(ByteArrayInputStream(bytes), bytes.size.toLong()) } @@ -538,11 +539,7 @@ abstract class ImageProvider { exif.setAttribute(ExifInterface.TAG_DATETIME, dateString) exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateString) - val offsetInMinutes = TimeZone.getDefault().getOffset(dateTimeMillis) / 60000 - val offsetSign = if (offsetInMinutes < 0) "-" else "+" - val offsetHours = "${offsetInMinutes / 60}".padStart(2, '0') - val offsetMinutes = "${offsetInMinutes % 60}".padStart(2, '0') - val timeZoneString = "$offsetSign$offsetHours:$offsetMinutes" + val timeZoneString = getTimeZoneString(TimeZone.getDefault(), dateTimeMillis) exif.setAttribute(ExifInterface.TAG_OFFSET_TIME, timeZoneString) exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, timeZoneString) @@ -1387,6 +1384,15 @@ abstract class ImageProvider { false } } + + fun getTimeZoneString(timeZone: TimeZone, dateTimeMillis: Long): String { + val offset = timeZone.getOffset(dateTimeMillis) + val offsetInMinutes = offset.absoluteValue / 60000 + val offsetSign = if (offset < 0) "-" else "+" + val offsetHours = "${offsetInMinutes / 60}".padStart(2, '0') + val offsetMinutes = "${offsetInMinutes % 60}".padStart(2, '0') + return "$offsetSign$offsetHours:$offsetMinutes" + } } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index 3983ff52e..1dd700ebf 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -328,7 +328,6 @@ class MediaStoreImageProvider : ImageProvider() { Log.d(LOG_TAG, "delete [permission:doc, file exists after content delete] document at uri=$uri path=$path") val df = StorageUtils.getDocumentFile(contextWrapper, path, uri) - @Suppress("BlockingMethodInNonBlockingContext") if (df != null && df.delete()) { scanObsoletePath(contextWrapper, uri, path, mimeType) return @@ -726,7 +725,6 @@ class MediaStoreImageProvider : ImageProvider() { val df = StorageUtils.getDocumentFile(activity, oldPath, oldMediaUri) df ?: throw Exception("failed to get document at path=$oldPath") - @Suppress("BlockingMethodInNonBlockingContext") val renamed = df.renameTo(newFile.name) if (!renamed) { throw Exception("failed to rename document at path=$oldPath") diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt index 4aa362a1b..fb75f47ff 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt @@ -5,9 +5,14 @@ import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo +import android.graphics.BitmapRegionDecoder +import android.location.Address +import android.location.Geocoder import android.os.Build import android.os.Parcelable import android.view.Display +import java.io.IOException +import java.io.InputStream inline fun Intent.getParcelableExtraCompat(name: String): T? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -44,3 +49,44 @@ fun PackageManager.queryIntentActivitiesCompat(intent: Intent, flags: Int): List queryIntentActivities(intent, flags) } } + +fun Geocoder.getFromLocationCompat( + latitude: Double, + longitude: Double, + maxResults: Int, + processAddresses: (addresses: List
) -> Unit, + onError: (errorCode: String, errorMessage: String?, errorDetails: Any?) -> Unit, +) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getFromLocation(latitude, longitude, maxResults, object : Geocoder.GeocodeListener { + override fun onGeocode(addresses: List) = processAddresses(addresses.filterNotNull()) + + override fun onError(errorMessage: String?) { + onError("getAddress-asyncerror", "failed to get address", errorMessage) + } + }) + } else { + try { + @Suppress("deprecation") + val addresses = getFromLocation(latitude, longitude, maxResults) ?: ArrayList() + processAddresses(addresses) + } catch (e: IOException) { + // `grpc failed`, etc. + onError("getAddress-network", "failed to get address because of network issues", e.message) + } catch (e: Exception) { + onError("getAddress-exception", "failed to get address", e.message) + } + } +} + +object BitmapRegionDecoderCompat { + @Throws(IOException::class) + fun newInstance(input: InputStream): BitmapRegionDecoder? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + BitmapRegionDecoder.newInstance(input) + } else { + @Suppress("deprecation") + BitmapRegionDecoder.newInstance(input, false) + } + } +} diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt index 6863489fd..91e5dd4d5 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt @@ -1,6 +1,5 @@ package deckers.thibault.aves.utils -import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.Intent @@ -177,14 +176,10 @@ object PermissionManager { val accessibleDirs = HashSet(getGrantedDirs(context)) accessibleDirs.addAll(context.getExternalFilesDirs(null).filterNotNull().map { it.path }) - // until API 18 / Android 4.3 / Jelly Bean MR2, removable storage is accessible by default like primary storage // from API 19 / Android 4.4 / KitKat, removable storage requires access permission, at the file level // from API 21 / Android 5.0 / Lollipop, removable storage requires access permission, but directory access grant is possible // from API 30 / Android 11 / R, any storage requires access permission - @SuppressLint("ObsoleteSdkInt") - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) { - accessibleDirs.addAll(StorageUtils.getVolumePaths(context)) - } else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { accessibleDirs.add(StorageUtils.getPrimaryVolumePath(context)) } return accessibleDirs diff --git a/android/app/src/main/res/values-ar/strings.xml b/android/app/src/main/res/values-ar/strings.xml index d55481fb9..0d6515695 100644 --- a/android/app/src/main/res/values-ar/strings.xml +++ b/android/app/src/main/res/values-ar/strings.xml @@ -5,7 +5,8 @@ البحث الفيديوهات فحص الوسائط - فحص الصور والفيديوهات يتم فحص الوسائط إيقاف + الوضع الآمن + أيفيس \ No newline at end of file diff --git a/android/app/src/main/res/values-ckb/strings.xml b/android/app/src/main/res/values-ckb/strings.xml index 13c4df775..fb8b36a87 100644 --- a/android/app/src/main/res/values-ckb/strings.xml +++ b/android/app/src/main/res/values-ckb/strings.xml @@ -6,7 +6,6 @@ گەڕان ڤیدیۆ گەڕان بۆ فایل - گەڕان بۆ وێنە و ڤیدیۆ گەڕان بۆ فایلەکان وەستاندن \ No newline at end of file diff --git a/android/app/src/main/res/values-cs/strings.xml b/android/app/src/main/res/values-cs/strings.xml index 02638c56a..556ce9d94 100644 --- a/android/app/src/main/res/values-cs/strings.xml +++ b/android/app/src/main/res/values-cs/strings.xml @@ -5,7 +5,6 @@ Hledat Videa Prohledat média - Prohledat obrázky a videa Prohledávání médií Zastavit Fotorámeček diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml index 1ecb251c5..67f33df47 100644 --- a/android/app/src/main/res/values-de/strings.xml +++ b/android/app/src/main/res/values-de/strings.xml @@ -6,7 +6,6 @@ Suche Videos Analyse von Medien - Bilder & Videos scannen Medien scannen Abbrechen Sicherer Modus diff --git a/android/app/src/main/res/values-el/strings.xml b/android/app/src/main/res/values-el/strings.xml index 2635a337b..6a15053be 100644 --- a/android/app/src/main/res/values-el/strings.xml +++ b/android/app/src/main/res/values-el/strings.xml @@ -6,7 +6,7 @@ Αναζήτηση Βίντεο Σάρωση πολυμέσων - Σάρωση εικόνων & Βίντεο Σάρωση στοιχείων Διακοπή + Ασφαλής κατάσταση λειτουργίας \ No newline at end of file diff --git a/android/app/src/main/res/values-es/strings.xml b/android/app/src/main/res/values-es/strings.xml index b2364e4dd..9fbcf09b4 100644 --- a/android/app/src/main/res/values-es/strings.xml +++ b/android/app/src/main/res/values-es/strings.xml @@ -6,7 +6,6 @@ Búsqueda Vídeos Explorar medios - Explorar imágenes & videos Explorando medios Anular Modo seguro diff --git a/android/app/src/main/res/values-eu/strings.xml b/android/app/src/main/res/values-eu/strings.xml index 92a69bac7..732dd1c98 100644 --- a/android/app/src/main/res/values-eu/strings.xml +++ b/android/app/src/main/res/values-eu/strings.xml @@ -3,7 +3,6 @@ Bilatu Bideoak Argazki-markoa - Irudiak eta bideoak eskaneatu Horma-papera Media eskaneatu Gelditu diff --git a/android/app/src/main/res/values-fa/strings.xml b/android/app/src/main/res/values-fa/strings.xml index 5b1e2f8ea..e69a7f2b5 100644 --- a/android/app/src/main/res/values-fa/strings.xml +++ b/android/app/src/main/res/values-fa/strings.xml @@ -2,7 +2,6 @@ ویدئو ها کنکاش رسانه - کنکاش تصاویر و ویدئو ها جستجو کاغذدیواری در حال کنکاش رسانه‌ها diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml index 0243698d8..fa821f4ea 100644 --- a/android/app/src/main/res/values-fr/strings.xml +++ b/android/app/src/main/res/values-fr/strings.xml @@ -6,7 +6,6 @@ Recherche Vidéos Analyse des images - Analyse des images & vidéos Analyse des images Annuler Mode sans échec diff --git a/android/app/src/main/res/values-gl/strings.xml b/android/app/src/main/res/values-gl/strings.xml index 6be7c113e..1049c3728 100644 --- a/android/app/src/main/res/values-gl/strings.xml +++ b/android/app/src/main/res/values-gl/strings.xml @@ -6,7 +6,6 @@ Procura Vídeos Escaneo multimedia - Escanealas imaxes e os vídeos Escaneando medios Pare \ No newline at end of file diff --git a/android/app/src/main/res/values-hi/strings.xml b/android/app/src/main/res/values-hi/strings.xml index 615529ee1..0447c8eb1 100644 --- a/android/app/src/main/res/values-hi/strings.xml +++ b/android/app/src/main/res/values-hi/strings.xml @@ -8,5 +8,4 @@ मीडिया जाँचे ऐवीज वीडियो - छवि & वीडियो जाँचे \ No newline at end of file diff --git a/android/app/src/main/res/values-hu/strings.xml b/android/app/src/main/res/values-hu/strings.xml index 5d4734530..4475c608f 100644 --- a/android/app/src/main/res/values-hu/strings.xml +++ b/android/app/src/main/res/values-hu/strings.xml @@ -8,6 +8,5 @@ Fotó keret Biztonsági üzemmód Tartalom keresése - Képek és videók keresése Média beolvasása \ No newline at end of file diff --git a/android/app/src/main/res/values-id/strings.xml b/android/app/src/main/res/values-id/strings.xml index c8067fc0e..29fc0d54c 100644 --- a/android/app/src/main/res/values-id/strings.xml +++ b/android/app/src/main/res/values-id/strings.xml @@ -6,7 +6,6 @@ Cari Video Pindai media - Pindai gambar & video Memindai media Berhenti Mode aman diff --git a/android/app/src/main/res/values-it/strings.xml b/android/app/src/main/res/values-it/strings.xml index 1f254b126..ac62cd864 100644 --- a/android/app/src/main/res/values-it/strings.xml +++ b/android/app/src/main/res/values-it/strings.xml @@ -6,7 +6,6 @@ Ricerca Video Scansione media - Scansione immagini & videos Scansione in corso Annulla Modalità provvisoria diff --git a/android/app/src/main/res/values-iw/strings.xml b/android/app/src/main/res/values-iw/strings.xml index fa2bc3053..8cee373a7 100644 --- a/android/app/src/main/res/values-iw/strings.xml +++ b/android/app/src/main/res/values-iw/strings.xml @@ -6,7 +6,6 @@ חיפוש סרטים סריקת מדיה - סרוק תמונות וסרטים סורק מדיה הפסק \ No newline at end of file diff --git a/android/app/src/main/res/values-ja/strings.xml b/android/app/src/main/res/values-ja/strings.xml index 7204d99c5..d5f7e4ed4 100644 --- a/android/app/src/main/res/values-ja/strings.xml +++ b/android/app/src/main/res/values-ja/strings.xml @@ -6,7 +6,6 @@ 検索 動画 メディアスキャン - 画像と動画をスキャン メディアをスキャン中 停止 セーフモード diff --git a/android/app/src/main/res/values-ko/strings.xml b/android/app/src/main/res/values-ko/strings.xml index 0c064c633..9f19c1398 100644 --- a/android/app/src/main/res/values-ko/strings.xml +++ b/android/app/src/main/res/values-ko/strings.xml @@ -6,7 +6,6 @@ 검색 동영상 미디어 분석 - 사진과 동영상 분석 미디어 분석 취소 안전 모드 diff --git a/android/app/src/main/res/values-lt/strings.xml b/android/app/src/main/res/values-lt/strings.xml index b1ce60a3c..d20202da5 100644 --- a/android/app/src/main/res/values-lt/strings.xml +++ b/android/app/src/main/res/values-lt/strings.xml @@ -1,6 +1,5 @@ - Nuskaityti paveikslėlius ir vaizdo įrašus Ekrano paveikslėlis Vaizdo įrašai Aves diff --git a/android/app/src/main/res/values-ml/strings.xml b/android/app/src/main/res/values-ml/strings.xml new file mode 100644 index 000000000..3df6e6419 --- /dev/null +++ b/android/app/src/main/res/values-ml/strings.xml @@ -0,0 +1,9 @@ + + + ഏവ്സ് + ചുമർ ചിത്രം + തിരയുക + വീഡിയോകൾ + മാധ്യമ സൂക്ഷ്മപരിശോധന + നിർത്തുക + \ No newline at end of file diff --git a/android/app/src/main/res/values-nb-rNO/strings.xml b/android/app/src/main/res/values-nb-rNO/strings.xml index d1759646b..f9ec90655 100644 --- a/android/app/src/main/res/values-nb-rNO/strings.xml +++ b/android/app/src/main/res/values-nb-rNO/strings.xml @@ -3,7 +3,6 @@ Aves Videoer Mediaskanning - Skann bilder og videoer Skanning av media Bilderamme Bakgrunnsbilde diff --git a/android/app/src/main/res/values-nl/strings.xml b/android/app/src/main/res/values-nl/strings.xml index b8015ce4f..aae728ba9 100644 --- a/android/app/src/main/res/values-nl/strings.xml +++ b/android/app/src/main/res/values-nl/strings.xml @@ -6,7 +6,6 @@ Zoeken Video’s Media indexeren - Indexeren van afdbeeldingen & video’s Indexeren van media Stop \ No newline at end of file diff --git a/android/app/src/main/res/values-nn/strings.xml b/android/app/src/main/res/values-nn/strings.xml index 8f62f5869..615c9eb6b 100644 --- a/android/app/src/main/res/values-nn/strings.xml +++ b/android/app/src/main/res/values-nn/strings.xml @@ -6,7 +6,6 @@ Søk Videoar Mediasøking - Søk igjennom bilete og videoar Søkjer igjennom media Stogg \ No newline at end of file diff --git a/android/app/src/main/res/values-pl/strings.xml b/android/app/src/main/res/values-pl/strings.xml index ed2f0b129..f88e6931e 100644 --- a/android/app/src/main/res/values-pl/strings.xml +++ b/android/app/src/main/res/values-pl/strings.xml @@ -4,7 +4,6 @@ Szukaj Wideo Przeskanuj multimedia - Przeskanuj obrazy oraz wideo Skanowanie multimediów Zatrzymaj Aves diff --git a/android/app/src/main/res/values-pt/strings.xml b/android/app/src/main/res/values-pt/strings.xml index 17abf2439..0e887b1bb 100644 --- a/android/app/src/main/res/values-pt/strings.xml +++ b/android/app/src/main/res/values-pt/strings.xml @@ -6,7 +6,6 @@ Procurar Vídeos Digitalização de mídia - Digitalizar imagens & vídeos Digitalizando mídia Pare \ No newline at end of file diff --git a/android/app/src/main/res/values-ro/strings.xml b/android/app/src/main/res/values-ro/strings.xml index bb3475bf8..e498222a8 100644 --- a/android/app/src/main/res/values-ro/strings.xml +++ b/android/app/src/main/res/values-ro/strings.xml @@ -5,7 +5,6 @@ Tapet Videoclipuri Scanare media - Scanați imagini și videoclipuri Scanarea suporturilor Stop Căutare diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml index 84259422b..a0e991e96 100644 --- a/android/app/src/main/res/values-ru/strings.xml +++ b/android/app/src/main/res/values-ru/strings.xml @@ -6,7 +6,6 @@ Поиск Видео Сканировать медия - Сканировать изображения и видео Сканирование медиа Стоп Безопасный режим diff --git a/android/app/src/main/res/values-sk/strings.xml b/android/app/src/main/res/values-sk/strings.xml index 08fd7dbe9..9a8ac434a 100644 --- a/android/app/src/main/res/values-sk/strings.xml +++ b/android/app/src/main/res/values-sk/strings.xml @@ -7,6 +7,5 @@ Videá Zastaviť Skenovanie médií - Skenovanie obrázkov & videí Skenovanie média \ No newline at end of file diff --git a/android/app/src/main/res/values-th/strings.xml b/android/app/src/main/res/values-th/strings.xml index b582dd0f7..2ac92953e 100644 --- a/android/app/src/main/res/values-th/strings.xml +++ b/android/app/src/main/res/values-th/strings.xml @@ -6,7 +6,6 @@ ค้นหา วิดีโอ สแกนสื่อบันเทิง - สแกนรูปภาพและวิดีโอ กำลังสแกนสื่อบันเทิง หยุด \ No newline at end of file diff --git a/android/app/src/main/res/values-tr/strings.xml b/android/app/src/main/res/values-tr/strings.xml index a358643e8..e516cc30b 100644 --- a/android/app/src/main/res/values-tr/strings.xml +++ b/android/app/src/main/res/values-tr/strings.xml @@ -6,7 +6,6 @@ Arama Videolar Medya tarama - Görüntüleri ve videoları tarayın Medya taranıyor Durdur \ No newline at end of file diff --git a/android/app/src/main/res/values-uk/strings.xml b/android/app/src/main/res/values-uk/strings.xml index b6c0c0a68..71b3f03aa 100644 --- a/android/app/src/main/res/values-uk/strings.xml +++ b/android/app/src/main/res/values-uk/strings.xml @@ -5,7 +5,6 @@ Пошук Відео Сканувати медіа - Сканувати зображення та відео Стоп Фоторамка Сканування медіа diff --git a/android/app/src/main/res/values-zh-rTW/strings.xml b/android/app/src/main/res/values-zh-rTW/strings.xml index 8071ccad2..7de20ea26 100644 --- a/android/app/src/main/res/values-zh-rTW/strings.xml +++ b/android/app/src/main/res/values-zh-rTW/strings.xml @@ -7,6 +7,5 @@ 相框 搜尋 媒體掃描 - 掃描圖片和影片 停止 \ No newline at end of file diff --git a/android/app/src/main/res/values-zh/strings.xml b/android/app/src/main/res/values-zh/strings.xml index 7b454e26b..e6ed4d076 100644 --- a/android/app/src/main/res/values-zh/strings.xml +++ b/android/app/src/main/res/values-zh/strings.xml @@ -6,7 +6,6 @@ 搜索 视频 媒体扫描 - 扫描图像 & 视频 正在扫描媒体库 停止 \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index f0f317c96..e77df79e1 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -7,7 +7,6 @@ Search Videos Media scan - Scan images & videos Scanning media Stop \ No newline at end of file diff --git a/android/app/src/test/kotlin/deckers/thibault/aves/model/provider/ImageProviderTest.kt b/android/app/src/test/kotlin/deckers/thibault/aves/model/provider/ImageProviderTest.kt new file mode 100644 index 000000000..705503c62 --- /dev/null +++ b/android/app/src/test/kotlin/deckers/thibault/aves/model/provider/ImageProviderTest.kt @@ -0,0 +1,18 @@ +package deckers.thibault.aves.model.provider + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.Month +import java.util.TimeZone + +class ImageProviderTest { + @Test + fun imageProvider_CorrectEmailSimple_ReturnsTrue() { + val date = LocalDate.of(1990, Month.FEBRUARY, 11).toEpochDay() + assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("Europe/Paris"), date), "+01:00") + assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("UTC"), date), "+00:00") + assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("Asia/Kolkata"), date), "+05:30") + assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("America/Chicago"), date), "-06:00") + } +} diff --git a/android/build.gradle b/android/build.gradle index c9ccd0367..c173b9fdb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,6 +1,11 @@ buildscript { ext { - kotlin_version = '1.8.0' + kotlin_version = '1.8.21' + agp_version = '8.0.1' + glide_version = '4.15.1' + // AppGallery Connect plugin versions: https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-sdk-changenotes-0000001058732550 + // TODO TLAD AppGallery Connect plugin v1.9.0.300 does not support Gradle 8+ + huawei_agconnect_version = '1.9.0.300' abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4] useCrashlytics = gradle.startParameter.taskNames.any { task -> task.containsIgnoreCase("play") } useHms = gradle.startParameter.taskNames.any { task -> task.containsIgnoreCase("huawei") } @@ -17,7 +22,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath "com.android.tools.build:gradle:$agp_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" if (useCrashlytics) { @@ -28,7 +33,7 @@ buildscript { if (useHms) { // HMS (used by some flavors only) - classpath 'com.huawei.agconnect:agcp:1.8.0.300' + classpath "com.huawei.agconnect:agcp:$huawei_agconnect_version" } } } @@ -57,6 +62,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register('clean', Delete) { delete rootProject.buildDir } diff --git a/android/gradle.properties b/android/gradle.properties index 3422ffa05..9a6e41359 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -15,3 +15,10 @@ android.useAndroidX=true android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false + +# fix for AppGallery Connect plugin which does not support yet Gradle 8 +# cf https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-common-faq-0000001063210244#section17273113244910 +apmsInstrumentationEnabled=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 3c472b99c..8bc9958ab 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip diff --git a/fastlane/metadata/android/en-US/changelogs/98.txt b/fastlane/metadata/android/en-US/changelogs/98.txt new file mode 100644 index 000000000..58bdc5c44 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/98.txt @@ -0,0 +1,5 @@ +In v1.8.7: +- play your animated PNGs +- set your home to the Tags page +- enjoy the app in Norwegian (Nynorsk) +Full changelog available on GitHub \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/9801.txt b/fastlane/metadata/android/en-US/changelogs/9801.txt new file mode 100644 index 000000000..58bdc5c44 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/9801.txt @@ -0,0 +1,5 @@ +In v1.8.7: +- play your animated PNGs +- set your home to the Tags page +- enjoy the app in Norwegian (Nynorsk) +Full changelog available on GitHub \ No newline at end of file diff --git a/fastlane/metadata/android/eu/full_description.txt b/fastlane/metadata/android/eu/full_description.txt index b0d5229c3..e615df16a 100644 --- a/fastlane/metadata/android/eu/full_description.txt +++ b/fastlane/metadata/android/eu/full_description.txt @@ -1,5 +1,5 @@ -Aves aplikazioak mota guztitako irudi eta bideoak, nahiz ohiko zure JPEG eta MP4 fitxategiak eta exotikoagoak diren orri ugaritako TIFF, SVG, AVI zaharrak eta are gehiago maneiatzen ditu! Zure media-bilduma eskaneatzen du mugimendu-argazkiak,panoramikak (argazki esferikoak bezala ere ezagunak), 360°-ko bideoak, baita GeoTIFF fitxategiak ere. +Aves aplikazioak mota guztitako irudi eta bideoak, nahiz ohiko zure JPEG eta MP4 fitxategiak eta exotikoagoak diren orri ugaritako TIFF, SVG, AVI zaharrak eta are gehiago maneiatzen ditu! Zure media-bilduma eskaneatzen du mugimendu-argazkiak, panoramikak (argazki esferikoak bezala ere ezagunak), 360°-ko bideoak, baita GeoTIFF fitxategiak ere. Nabigazioa eta bilaketa Aves aplikazioaren zati garrantzitsu bat da. Helburua, erabiltzaileek albumetatik argazkietara, etiketetara, mapetara, etab. modu errazean mugi ahal izatea da. -Aves Androidera (KitKatetik Android 13ra, Android TV barne) egiten da ezaugarri ugarirekin: widgetak, aplikazioko lasterbideak, pantaila-babeslea eta bilaketa globala. Baita ere, media-bisore edo -hautagailu bezala ere erabil daiteke. \ No newline at end of file +Aves Androidera (KitKatetik Android 13ra, Android TV barne) egiten da ezaugarri ugarirekin: widgetak, aplikazioko lasterbideak, pantaila-babeslea eta bilaketa globala. Baita ere, media-bisore edo -hautagailu bezala erabil daiteke. \ No newline at end of file diff --git a/fastlane/metadata/android/ml/full_description.txt b/fastlane/metadata/android/ml/full_description.txt new file mode 100644 index 000000000..6c92748f8 --- /dev/null +++ b/fastlane/metadata/android/ml/full_description.txt @@ -0,0 +1,5 @@ +Aves can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like multi-page TIFFs, SVGs, old AVIs and more! It scans your media collection to identify motion photos, panoramas (aka photo spheres), 360° videos, as well as GeoTIFF files. + +Navigation and search is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc. + +Aves integrates with Android (from KitKat to Android 13, including Android TV) with features such as widgets, app shortcuts, screen saver and global search handling. It also works as a media viewer and picker. \ No newline at end of file diff --git a/fastlane/metadata/android/ml/short_description.txt b/fastlane/metadata/android/ml/short_description.txt new file mode 100644 index 000000000..8c9445bd5 --- /dev/null +++ b/fastlane/metadata/android/ml/short_description.txt @@ -0,0 +1 @@ +Gallery and metadata explorer \ No newline at end of file diff --git a/fastlane/metadata/android/nn/images/featureGraphic.png b/fastlane/metadata/android/nn/images/featureGraphic.png new file mode 100644 index 000000000..e9e800f6b Binary files /dev/null and b/fastlane/metadata/android/nn/images/featureGraphic.png differ diff --git a/fastlane/metadata/android/nn/images/phoneScreenshots/1.png b/fastlane/metadata/android/nn/images/phoneScreenshots/1.png new file mode 100644 index 000000000..8d90d9b37 Binary files /dev/null and b/fastlane/metadata/android/nn/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/nn/images/phoneScreenshots/2.png b/fastlane/metadata/android/nn/images/phoneScreenshots/2.png new file mode 100644 index 000000000..9fd360a64 Binary files /dev/null and b/fastlane/metadata/android/nn/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/nn/images/phoneScreenshots/3.png b/fastlane/metadata/android/nn/images/phoneScreenshots/3.png new file mode 100644 index 000000000..927ddecd0 Binary files /dev/null and b/fastlane/metadata/android/nn/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/nn/images/phoneScreenshots/4.png b/fastlane/metadata/android/nn/images/phoneScreenshots/4.png new file mode 100644 index 000000000..825374766 Binary files /dev/null and b/fastlane/metadata/android/nn/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/nn/images/phoneScreenshots/5.png b/fastlane/metadata/android/nn/images/phoneScreenshots/5.png new file mode 100644 index 000000000..c9ba7c3e0 Binary files /dev/null and b/fastlane/metadata/android/nn/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/nn/images/phoneScreenshots/6.png b/fastlane/metadata/android/nn/images/phoneScreenshots/6.png new file mode 100644 index 000000000..be4769665 Binary files /dev/null and b/fastlane/metadata/android/nn/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/nn/images/phoneScreenshots/7.png b/fastlane/metadata/android/nn/images/phoneScreenshots/7.png new file mode 100644 index 000000000..633599ce2 Binary files /dev/null and b/fastlane/metadata/android/nn/images/phoneScreenshots/7.png differ diff --git a/lib/app_mode.dart b/lib/app_mode.dart index 99dd00ef4..8135170b4 100644 --- a/lib/app_mode.dart +++ b/lib/app_mode.dart @@ -9,6 +9,7 @@ enum AppMode { setWallpaper, slideshow, view, + edit, } extension ExtraAppMode on AppMode { diff --git a/lib/image_providers/app_icon_image_provider.dart b/lib/image_providers/app_icon_image_provider.dart index a0aaa32b2..545fd1c45 100644 --- a/lib/image_providers/app_icon_image_provider.dart +++ b/lib/image_providers/app_icon_image_provider.dart @@ -27,7 +27,7 @@ class AppIconImage extends ImageProvider { } @override - ImageStreamCompleter loadBuffer(AppIconImageKey key, DecoderBufferCallback decode) { + ImageStreamCompleter loadImage(AppIconImageKey key, ImageDecoderCallback decode) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key, decode), scale: key.scale, @@ -37,7 +37,7 @@ class AppIconImage extends ImageProvider { ); } - Future _loadAsync(AppIconImageKey key, DecoderBufferCallback decode) async { + Future _loadAsync(AppIconImageKey key, ImageDecoderCallback decode) async { try { final bytes = await appService.getAppIcon(key.packageName, key.size); final buffer = await ui.ImmutableBuffer.fromUint8List(bytes.isEmpty ? kTransparentImage : bytes); diff --git a/lib/image_providers/region_provider.dart b/lib/image_providers/region_provider.dart index 135215324..dc85ccaa1 100644 --- a/lib/image_providers/region_provider.dart +++ b/lib/image_providers/region_provider.dart @@ -18,7 +18,7 @@ class RegionProvider extends ImageProvider { } @override - ImageStreamCompleter loadBuffer(RegionProviderKey key, DecoderBufferCallback decode) { + ImageStreamCompleter loadImage(RegionProviderKey key, ImageDecoderCallback decode) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key, decode), scale: 1.0, @@ -28,7 +28,7 @@ class RegionProvider extends ImageProvider { ); } - Future _loadAsync(RegionProviderKey key, DecoderBufferCallback decode) async { + Future _loadAsync(RegionProviderKey key, ImageDecoderCallback decode) async { final uri = key.uri; final mimeType = key.mimeType; final pageId = key.pageId; diff --git a/lib/image_providers/thumbnail_provider.dart b/lib/image_providers/thumbnail_provider.dart index bf13c32bc..33f05fec4 100644 --- a/lib/image_providers/thumbnail_provider.dart +++ b/lib/image_providers/thumbnail_provider.dart @@ -19,7 +19,7 @@ class ThumbnailProvider extends ImageProvider { } @override - ImageStreamCompleter loadBuffer(ThumbnailProviderKey key, DecoderBufferCallback decode) { + ImageStreamCompleter loadImage(ThumbnailProviderKey key, ImageDecoderCallback decode) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key, decode), scale: 1.0, @@ -30,7 +30,7 @@ class ThumbnailProvider extends ImageProvider { ); } - Future _loadAsync(ThumbnailProviderKey key, DecoderBufferCallback decode) async { + Future _loadAsync(ThumbnailProviderKey key, ImageDecoderCallback decode) async { final uri = key.uri; final mimeType = key.mimeType; final pageId = key.pageId; diff --git a/lib/image_providers/uri_image_provider.dart b/lib/image_providers/uri_image_provider.dart index ffcb622f7..38842e742 100644 --- a/lib/image_providers/uri_image_provider.dart +++ b/lib/image_providers/uri_image_provider.dart @@ -32,7 +32,7 @@ class UriImage extends ImageProvider with EquatableMixin { } @override - ImageStreamCompleter loadBuffer(UriImage key, DecoderBufferCallback decode) { + ImageStreamCompleter loadImage(UriImage key, ImageDecoderCallback decode) { final chunkEvents = StreamController(); return MultiFrameImageStreamCompleter( @@ -45,7 +45,7 @@ class UriImage extends ImageProvider with EquatableMixin { ); } - Future _loadAsync(UriImage key, DecoderBufferCallback decode, StreamController chunkEvents) async { + Future _loadAsync(UriImage key, ImageDecoderCallback decode, StreamController chunkEvents) async { assert(key == this); try { diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 557e12b6a..73b65e109 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -39,7 +39,7 @@ "@changeTooltip": {}, "actionRemove": "إزالة", "@actionRemove": {}, - "appName": "Aves", + "appName": "أيفيس", "@appName": {}, "welcomeOptional": "اختياري", "@welcomeOptional": {}, @@ -51,7 +51,7 @@ "@cancelTooltip": {}, "previousTooltip": "السابق", "@previousTooltip": {}, - "welcomeMessage": "مرحبا بكم في Aves", + "welcomeMessage": "مرحبا بكم في أيفيس", "@welcomeMessage": {}, "applyButtonLabel": "تطبيق", "@applyButtonLabel": {}, @@ -68,5 +68,7 @@ "hideTooltip": "إخفاء", "@hideTooltip": {}, "tagEditorPageAddTagTooltip": "إضافة علامة", - "@tagEditorPageAddTagTooltip": {} + "@tagEditorPageAddTagTooltip": {}, + "albumScreenRecordings": "تسجيل الشاشة", + "@albumScreenRecordings": {} } diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index c388db77d..1f52e0060 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -1452,5 +1452,39 @@ "tagPlaceholderState": "Stát", "@tagPlaceholderState": {}, "settingsVideoBackgroundModeDialogTitle": "Režim na pozadí", - "@settingsVideoBackgroundModeDialogTitle": {} + "@settingsVideoBackgroundModeDialogTitle": {}, + "saveCopyButtonLabel": "ULOŽIT KOPII", + "@saveCopyButtonLabel": {}, + "applyTooltip": "Použít", + "@applyTooltip": {}, + "editorTransformCrop": "Oříznout", + "@editorTransformCrop": {}, + "editorTransformRotate": "Otočit", + "@editorTransformRotate": {}, + "cropAspectRatioOriginal": "Originál", + "@cropAspectRatioOriginal": {}, + "widgetTapUpdateWidget": "Aktualizovat widget", + "@widgetTapUpdateWidget": {}, + "settingsVideoResumptionModeDialogTitle": "Obnovit přehrávání", + "@settingsVideoResumptionModeDialogTitle": {}, + "settingsAskEverytime": "Vždy se zeptat", + "@settingsAskEverytime": {}, + "tagEditorDiscardDialogMessage": "Chcete zrušit změny?", + "@tagEditorDiscardDialogMessage": {}, + "maxBrightnessNever": "Nikdy", + "@maxBrightnessNever": {}, + "maxBrightnessAlways": "Vždy", + "@maxBrightnessAlways": {}, + "videoResumptionModeNever": "Nikdy", + "@videoResumptionModeNever": {}, + "videoResumptionModeAlways": "Vždy", + "@videoResumptionModeAlways": {}, + "exportEntryDialogQuality": "Kvalita", + "@exportEntryDialogQuality": {}, + "settingsVideoPlaybackTile": "Přehrávání", + "@settingsVideoPlaybackTile": {}, + "settingsVideoPlaybackPageTitle": "Přehrávání", + "@settingsVideoPlaybackPageTitle": {}, + "settingsVideoResumptionModeTile": "Obnovit přehrávání", + "@settingsVideoResumptionModeTile": {} } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9e83cf533..7512c6b81 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -303,7 +303,7 @@ "@wallpaperTargetHomeLock": {}, "widgetOpenPageHome": "Startseite öffnen", "@widgetOpenPageHome": {}, - "widgetOpenPageViewer": "Viewer öffnen", + "widgetOpenPageViewer": "Betrachter öffnen", "@widgetOpenPageViewer": {}, "albumTierNew": "Neu", "@albumTierNew": {}, @@ -1294,5 +1294,45 @@ "vaultBinUsageDialogMessage": "Einige Tresore verwenden den Papierkorb.", "@vaultBinUsageDialogMessage": {}, "pinDialogEnter": "PIN eingeben", - "@pinDialogEnter": {} + "@pinDialogEnter": {}, + "maxBrightnessNever": "Nie", + "@maxBrightnessNever": {}, + "maxBrightnessAlways": "Immer", + "@maxBrightnessAlways": {}, + "videoResumptionModeNever": "Nie", + "@videoResumptionModeNever": {}, + "videoResumptionModeAlways": "Immer", + "@videoResumptionModeAlways": {}, + "exportEntryDialogQuality": "Qualität", + "@exportEntryDialogQuality": {}, + "settingsAskEverytime": "Jedes Mal nachfragen", + "@settingsAskEverytime": {}, + "settingsVideoPlaybackTile": "Wiedergabe", + "@settingsVideoPlaybackTile": {}, + "settingsVideoPlaybackPageTitle": "Wiedergabe", + "@settingsVideoPlaybackPageTitle": {}, + "settingsVideoResumptionModeTile": "Wiedergabe fortsetzen", + "@settingsVideoResumptionModeTile": {}, + "settingsVideoResumptionModeDialogTitle": "Wiedergabe fortsetzen", + "@settingsVideoResumptionModeDialogTitle": {}, + "tagEditorDiscardDialogMessage": "Möchten Sie die Änderungen verwerfen?", + "@tagEditorDiscardDialogMessage": {}, + "saveCopyButtonLabel": "KOPIE SPEICHERN", + "@saveCopyButtonLabel": {}, + "applyTooltip": "Anwenden", + "@applyTooltip": {}, + "editorActionTransform": "Umwandeln", + "@editorActionTransform": {}, + "editorTransformCrop": "Zuschneiden", + "@editorTransformCrop": {}, + "editorTransformRotate": "Drehen", + "@editorTransformRotate": {}, + "cropAspectRatioFree": "Frei", + "@cropAspectRatioFree": {}, + "cropAspectRatioOriginal": "Original", + "@cropAspectRatioOriginal": {}, + "cropAspectRatioSquare": "Quadrat", + "@cropAspectRatioSquare": {}, + "widgetTapUpdateWidget": "Widget öffnen", + "@widgetTapUpdateWidget": {} } diff --git a/lib/l10n/app_el.arb b/lib/l10n/app_el.arb index 2716c9c6a..b31e3a925 100644 --- a/lib/l10n/app_el.arb +++ b/lib/l10n/app_el.arb @@ -1316,5 +1316,23 @@ "videoResumptionModeAlways": "Πάντα", "@videoResumptionModeAlways": {}, "settingsAskEverytime": "Ρωτήστε με κάθε φορά", - "@settingsAskEverytime": {} + "@settingsAskEverytime": {}, + "saveCopyButtonLabel": "ΑΠΟΘΗΚΕΥΣΗ ΑΝΤΙΓΡΑΦΟΥ", + "@saveCopyButtonLabel": {}, + "applyTooltip": "Εφαρμογή", + "@applyTooltip": {}, + "editorActionTransform": "Μετατροπή", + "@editorActionTransform": {}, + "editorTransformCrop": "Περικοπή", + "@editorTransformCrop": {}, + "editorTransformRotate": "Περιστροφή", + "@editorTransformRotate": {}, + "cropAspectRatioFree": "Ελεύθερη μορφή", + "@cropAspectRatioFree": {}, + "cropAspectRatioOriginal": "Αρχικό", + "@cropAspectRatioOriginal": {}, + "cropAspectRatioSquare": "Τετράγωνο", + "@cropAspectRatioSquare": {}, + "widgetTapUpdateWidget": "Ενημέρωση γραφικού στοιχείου", + "@widgetTapUpdateWidget": {} } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b8c620c98..bcd049bbd 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -50,7 +50,9 @@ "showButtonLabel": "SHOW", "hideButtonLabel": "HIDE", "continueButtonLabel": "CONTINUE", + "saveCopyButtonLabel": "SAVE COPY", + "applyTooltip": "Apply", "cancelTooltip": "Cancel", "changeTooltip": "Change", "clearTooltip": "Clear", @@ -141,6 +143,15 @@ "entryInfoActionExportMetadata": "Export metadata", "entryInfoActionRemoveLocation": "Remove location", + "editorActionTransform": "Transform", + + "editorTransformCrop": "Crop", + "editorTransformRotate": "Rotate", + + "cropAspectRatioFree": "Free", + "cropAspectRatioOriginal": "Original", + "cropAspectRatioSquare": "Square", + "filterAspectRatioLandscapeLabel": "Landscape", "filterAspectRatioPortraitLabel": "Portrait", "filterBinLabel": "Recycle bin", @@ -270,6 +281,7 @@ "widgetOpenPageHome": "Open home", "widgetOpenPageCollection": "Open collection", "widgetOpenPageViewer": "Open viewer", + "widgetTapUpdateWidget": "Update widget", "storageVolumeDescriptionFallbackPrimary": "Internal storage", "storageVolumeDescriptionFallbackNonPrimary": "SD card", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 5040c967f..98fca8db9 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1316,5 +1316,23 @@ "videoResumptionModeNever": "Nunca", "@videoResumptionModeNever": {}, "exportEntryDialogQuality": "Calidad", - "@exportEntryDialogQuality": {} + "@exportEntryDialogQuality": {}, + "saveCopyButtonLabel": "GUARDAR UNA COPIA", + "@saveCopyButtonLabel": {}, + "applyTooltip": "Aplicar", + "@applyTooltip": {}, + "editorTransformCrop": "Recortar", + "@editorTransformCrop": {}, + "editorTransformRotate": "Girar", + "@editorTransformRotate": {}, + "cropAspectRatioFree": "Libre", + "@cropAspectRatioFree": {}, + "cropAspectRatioSquare": "Cuadrado", + "@cropAspectRatioSquare": {}, + "editorActionTransform": "Transformar", + "@editorActionTransform": {}, + "cropAspectRatioOriginal": "Original", + "@cropAspectRatioOriginal": {}, + "widgetTapUpdateWidget": "Actualizar el widget", + "@widgetTapUpdateWidget": {} } diff --git a/lib/l10n/app_eu.arb b/lib/l10n/app_eu.arb index e1929c0bf..04972bb2e 100644 --- a/lib/l10n/app_eu.arb +++ b/lib/l10n/app_eu.arb @@ -1452,5 +1452,45 @@ "searchStatesSectionTitle": "Egoerak", "@searchStatesSectionTitle": {}, "statsTopStatesSectionTitle": "Egoera Nagusiak", - "@statsTopStatesSectionTitle": {} + "@statsTopStatesSectionTitle": {}, + "settingsAskEverytime": "Galdetu aldi oro", + "@settingsAskEverytime": {}, + "settingsVideoPlaybackTile": "Erreprodukzioa", + "@settingsVideoPlaybackTile": {}, + "maxBrightnessNever": "Inoiz", + "@maxBrightnessNever": {}, + "maxBrightnessAlways": "Beti", + "@maxBrightnessAlways": {}, + "videoResumptionModeAlways": "Beti", + "@videoResumptionModeAlways": {}, + "exportEntryDialogQuality": "Kalitatea", + "@exportEntryDialogQuality": {}, + "settingsVideoPlaybackPageTitle": "Erreprodukzioa", + "@settingsVideoPlaybackPageTitle": {}, + "settingsVideoResumptionModeTile": "Jarraitu erreprodukzioa", + "@settingsVideoResumptionModeTile": {}, + "tagEditorDiscardDialogMessage": "Aldaketak baztertu nahi dituzu?", + "@tagEditorDiscardDialogMessage": {}, + "videoResumptionModeNever": "Inoiz", + "@videoResumptionModeNever": {}, + "settingsVideoResumptionModeDialogTitle": "Jarraitu erreprodukzioa", + "@settingsVideoResumptionModeDialogTitle": {}, + "saveCopyButtonLabel": "GORDE KOPIA", + "@saveCopyButtonLabel": {}, + "applyTooltip": "Aplikatu", + "@applyTooltip": {}, + "editorActionTransform": "Eraldatu", + "@editorActionTransform": {}, + "editorTransformCrop": "Moztu", + "@editorTransformCrop": {}, + "editorTransformRotate": "Biratu", + "@editorTransformRotate": {}, + "cropAspectRatioFree": "Librea", + "@cropAspectRatioFree": {}, + "cropAspectRatioOriginal": "Jatorrizkoa", + "@cropAspectRatioOriginal": {}, + "cropAspectRatioSquare": "Karratua", + "@cropAspectRatioSquare": {}, + "widgetTapUpdateWidget": "Eguneratu widgeta", + "@widgetTapUpdateWidget": {} } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index a53802a31..a204ea360 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -17,7 +17,7 @@ "@timeDays": {}, "focalLength": "{length} mm", "@focalLength": {}, - "applyButtonLabel": "ENREGISTRER", + "applyButtonLabel": "APPLIQUER", "@applyButtonLabel": {}, "deleteButtonLabel": "SUPPRIMER", "@deleteButtonLabel": {}, @@ -1316,5 +1316,23 @@ "videoResumptionModeAlways": "Toujours", "@videoResumptionModeAlways": {}, "exportEntryDialogQuality": "Qualité", - "@exportEntryDialogQuality": {} + "@exportEntryDialogQuality": {}, + "saveCopyButtonLabel": "ENREGISTRER UNE COPIE", + "@saveCopyButtonLabel": {}, + "applyTooltip": "Appliquer", + "@applyTooltip": {}, + "editorActionTransform": "Transformation", + "@editorActionTransform": {}, + "editorTransformCrop": "Recadrage", + "@editorTransformCrop": {}, + "editorTransformRotate": "Rotation", + "@editorTransformRotate": {}, + "cropAspectRatioSquare": "Carré", + "@cropAspectRatioSquare": {}, + "cropAspectRatioFree": "Personnalisé", + "@cropAspectRatioFree": {}, + "cropAspectRatioOriginal": "Photo d’origine", + "@cropAspectRatioOriginal": {}, + "widgetTapUpdateWidget": "Mettre à jour le widget", + "@widgetTapUpdateWidget": {} } diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index ae5050bf8..35f720b45 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -119,7 +119,7 @@ "@aboutPageTitle": {}, "aboutLinkLicense": "Licensz", "@aboutLinkLicense": {}, - "aboutBugSectionTitle": "Hiba jelentés", + "aboutBugSectionTitle": "Hibajelentés", "@aboutBugSectionTitle": {}, "aboutBugCopyInfoButton": "Másolás", "@aboutBugCopyInfoButton": {}, @@ -1474,5 +1474,23 @@ "settingsVideoPlaybackTile": "Visszajátszás", "@settingsVideoPlaybackTile": {}, "exportEntryDialogQuality": "Minőség", - "@exportEntryDialogQuality": {} + "@exportEntryDialogQuality": {}, + "editorActionTransform": "Alakítás", + "@editorActionTransform": {}, + "cropAspectRatioOriginal": "Eredeti", + "@cropAspectRatioOriginal": {}, + "applyTooltip": "Alkalmaz", + "@applyTooltip": {}, + "saveCopyButtonLabel": "MÁSOLAT MENTÉSE", + "@saveCopyButtonLabel": {}, + "editorTransformCrop": "Vágás", + "@editorTransformCrop": {}, + "editorTransformRotate": "Forgatás", + "@editorTransformRotate": {}, + "cropAspectRatioSquare": "Négyzet", + "@cropAspectRatioSquare": {}, + "cropAspectRatioFree": "Kötetlen", + "@cropAspectRatioFree": {}, + "widgetTapUpdateWidget": "Widget frissítése", + "@widgetTapUpdateWidget": {} } diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 89fbcc29d..16eddb018 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -1304,5 +1304,35 @@ "videoResumptionModeNever": "Tidak pernah", "@videoResumptionModeNever": {}, "videoResumptionModeAlways": "Selalu", - "@videoResumptionModeAlways": {} + "@videoResumptionModeAlways": {}, + "settingsVideoResumptionModeTile": "Lanjutkan pemutaran", + "@settingsVideoResumptionModeTile": {}, + "settingsVideoResumptionModeDialogTitle": "Lanjutkan Pemutaran", + "@settingsVideoResumptionModeDialogTitle": {}, + "tagEditorDiscardDialogMessage": "Apakah Anda ingin membuang perubahan?", + "@tagEditorDiscardDialogMessage": {}, + "exportEntryDialogQuality": "Kualitas", + "@exportEntryDialogQuality": {}, + "settingsVideoPlaybackTile": "Pemutaran", + "@settingsVideoPlaybackTile": {}, + "settingsVideoPlaybackPageTitle": "Pemutaran", + "@settingsVideoPlaybackPageTitle": {}, + "cropAspectRatioOriginal": "Asli", + "@cropAspectRatioOriginal": {}, + "cropAspectRatioSquare": "Kotak", + "@cropAspectRatioSquare": {}, + "saveCopyButtonLabel": "SIMPAN SALINAN", + "@saveCopyButtonLabel": {}, + "applyTooltip": "Terapkan", + "@applyTooltip": {}, + "editorActionTransform": "Transformasi", + "@editorActionTransform": {}, + "editorTransformCrop": "Potong", + "@editorTransformCrop": {}, + "editorTransformRotate": "Putar", + "@editorTransformRotate": {}, + "cropAspectRatioFree": "Bebas", + "@cropAspectRatioFree": {}, + "widgetTapUpdateWidget": "Perbarui widget", + "@widgetTapUpdateWidget": {} } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index f8ea87c9d..d3d75fe63 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -17,7 +17,7 @@ "@timeDays": {}, "focalLength": "{length} mm", "@focalLength": {}, - "applyButtonLabel": "확인", + "applyButtonLabel": "적용", "@applyButtonLabel": {}, "deleteButtonLabel": "삭제", "@deleteButtonLabel": {}, @@ -1316,5 +1316,23 @@ "maxBrightnessNever": "작동 안 함", "@maxBrightnessNever": {}, "exportEntryDialogQuality": "품질", - "@exportEntryDialogQuality": {} + "@exportEntryDialogQuality": {}, + "saveCopyButtonLabel": "사본 저장", + "@saveCopyButtonLabel": {}, + "applyTooltip": "적용", + "@applyTooltip": {}, + "editorTransformCrop": "자르기", + "@editorTransformCrop": {}, + "editorTransformRotate": "회전", + "@editorTransformRotate": {}, + "cropAspectRatioFree": "사용자 맞춤 비율", + "@cropAspectRatioFree": {}, + "cropAspectRatioOriginal": "원본", + "@cropAspectRatioOriginal": {}, + "cropAspectRatioSquare": "정사각형", + "@cropAspectRatioSquare": {}, + "editorActionTransform": "변형", + "@editorActionTransform": {}, + "widgetTapUpdateWidget": "위젯 갱신", + "@widgetTapUpdateWidget": {} } diff --git a/lib/l10n/app_ml.arb b/lib/l10n/app_ml.arb new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/lib/l10n/app_ml.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/app_nn.arb b/lib/l10n/app_nn.arb index 11c247896..5276d79ea 100644 --- a/lib/l10n/app_nn.arb +++ b/lib/l10n/app_nn.arb @@ -64,13 +64,13 @@ "@actionRemove": {}, "resetTooltip": "Still attende", "@resetTooltip": {}, - "saveTooltip": "Gøym", + "saveTooltip": "Spar", "@saveTooltip": {}, "pickTooltip": "Vel", "@pickTooltip": {}, "chipActionUnpin": "losna ifrå toppen", "@chipActionUnpin": {}, - "chipActionRename": "Gjev nytt/nye namn", + "chipActionRename": "Byt namn", "@chipActionRename": {}, "chipActionSetCover": "Sett omslag", "@chipActionSetCover": {}, @@ -82,7 +82,7 @@ "@entryActionDelete": {}, "entryActionConvert": "Lag om", "@entryActionConvert": {}, - "entryActionRename": "Gjev nytt/nye namn", + "entryActionRename": "Byt namn", "@entryActionRename": {}, "entryActionRestore": "Gjenopprett", "@entryActionRestore": {}, @@ -114,11 +114,11 @@ "@entryActionRotateScreen": {}, "entryActionAddFavourite": "Lik", "@entryActionAddFavourite": {}, - "videoActionCaptureFrame": "Grip ramma", + "videoActionCaptureFrame": "Grip biletet", "@videoActionCaptureFrame": {}, "videoActionMute": "Døyv", "@videoActionMute": {}, - "videoActionUnmute": "Tak bort døyvinga", + "videoActionUnmute": "Ikkje døyv", "@videoActionUnmute": {}, "videoActionPause": "Stans", "@videoActionPause": {}, @@ -144,7 +144,7 @@ "@chipActionGoToAlbumPage": {}, "chipActionGoToCountryPage": "Vis i Land", "@chipActionGoToCountryPage": {}, - "chipActionGoToTagPage": "Vis i Merkelappar", + "chipActionGoToTagPage": "Vis i Merke", "@chipActionGoToTagPage": {}, "chipActionHide": "Skjul", "@chipActionHide": {}, @@ -176,15 +176,15 @@ "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Vis i Samling", "@slideshowActionShowInCollection": {}, - "entryInfoActionEditDate": "Brigd dato og tid", + "entryInfoActionEditDate": "Brigd datoen og tida", "@entryInfoActionEditDate": {}, "entryInfoActionEditTitleDescription": "Brigd namnet og utgreiinga", "@entryInfoActionEditTitleDescription": {}, "entryInfoActionEditRating": "Brigd omdøminga", "@entryInfoActionEditRating": {}, - "entryInfoActionEditTags": "Brigd merkelappar", + "entryInfoActionEditTags": "Brigd merka", "@entryInfoActionEditTags": {}, - "entryInfoActionExportMetadata": "Utfør metadata", + "entryInfoActionExportMetadata": "Før ut metadata", "@entryInfoActionExportMetadata": {}, "filterAspectRatioLandscapeLabel": "Liggjande", "@filterAspectRatioLandscapeLabel": {}, @@ -198,7 +198,7 @@ "@filterNoAddressLabel": {}, "filterNoRatingLabel": "Ikkje dømde", "@filterNoRatingLabel": {}, - "filterNoTagLabel": "Utan merkelappar", + "filterNoTagLabel": "Utan merke", "@filterNoTagLabel": {}, "filterNoTitleLabel": "Utan namn", "@filterNoTitleLabel": {}, @@ -206,7 +206,7 @@ "@filterRecentlyAddedLabel": {}, "filterRatingRejectedLabel": "Avslegen", "@filterRatingRejectedLabel": {}, - "filterTypeRawLabel": "Rå(tt)", + "filterTypeRawLabel": "Rå", "@filterTypeRawLabel": {}, "filterTypeSphericalVideoLabel": "360°-video", "@filterTypeSphericalVideoLabel": {}, @@ -252,7 +252,7 @@ "@mapStyleHuaweiNormal": {}, "mapStyleHuaweiTerrain": "Petal Maps (mark)", "@mapStyleHuaweiTerrain": {}, - "nameConflictStrategyRename": "Omkall", + "nameConflictStrategyRename": "Byt namn", "@nameConflictStrategyRename": {}, "nameConflictStrategyReplace": "Byt ut", "@nameConflictStrategyReplace": {}, @@ -264,7 +264,7 @@ "@keepScreenOnViewerOnly": {}, "keepScreenOnAlways": "Heile tida", "@keepScreenOnAlways": {}, - "accessibilityAnimationsRemove": "Hindra skjermrørsle", + "accessibilityAnimationsRemove": "Hindr skjermrørsler", "@accessibilityAnimationsRemove": {}, "subtitlePositionTop": "På toppen", "@subtitlePositionTop": {}, @@ -278,7 +278,7 @@ "@videoPlaybackWithSound": {}, "viewerTransitionZoomIn": "Auk", "@viewerTransitionZoomIn": {}, - "wallpaperTargetLock": "Låsaskjerm", + "wallpaperTargetLock": "Låseskjerm", "@wallpaperTargetLock": {}, "widgetDisplayedItemMostRecent": "Nylegaste", "@widgetDisplayedItemMostRecent": {}, @@ -312,7 +312,7 @@ } } }, - "entryInfoActionEditLocation": "Brigd stoda", + "entryInfoActionEditLocation": "Brigd stadsetjinga", "@entryInfoActionEditLocation": {}, "entryInfoActionRemoveMetadata": "Tak bort metadata", "@entryInfoActionRemoveMetadata": {}, @@ -359,7 +359,7 @@ "@newAlbumDialogNameLabel": {}, "durationDialogMinutes": "Minutt", "@durationDialogMinutes": {}, - "settingsThemeColorHighlights": "Farga framhevjingar", + "settingsThemeColorHighlights": "Léta framhevjingar", "@settingsThemeColorHighlights": {}, "viewerInfoBackToViewerTooltip": "Attende til vising", "@viewerInfoBackToViewerTooltip": {}, @@ -406,7 +406,7 @@ "@renameEntrySetPagePreviewSectionTitle": {}, "renameEntryDialogLabel": "Nytt namn", "@renameEntryDialogLabel": {}, - "editEntryDialogCopyFromItem": "Kopier ifrå anna element", + "editEntryDialogCopyFromItem": "Kopier ifrå annan ting", "@editEntryDialogCopyFromItem": {}, "editEntryDateDialogSourceFileModifiedDate": "Filbrigdedato", "@editEntryDateDialogSourceFileModifiedDate": {}, @@ -414,17 +414,17 @@ "@durationDialogHours": {}, "editEntryLocationDialogChooseOnMap": "Vel på kartet", "@editEntryLocationDialogChooseOnMap": {}, - "settingsLanguageTile": "Mål", + "settingsLanguageTile": "Skriftmål", "@settingsLanguageTile": {}, "settingsUnitSystemTile": "Einingar", "@settingsUnitSystemTile": {}, "settingsCoordinateFormatDialogTitle": "Koordinatformat", "@settingsCoordinateFormatDialogTitle": {}, - "settingsWidgetDisplayedItem": "Vist element", + "settingsWidgetDisplayedItem": "Vist ting", "@settingsWidgetDisplayedItem": {}, "statsTopAlbumsSectionTitle": "Topp-album", "@statsTopAlbumsSectionTitle": {}, - "statsTopTagsSectionTitle": "Toppmerkelappar", + "statsTopTagsSectionTitle": "Toppmerke", "@statsTopTagsSectionTitle": {}, "viewerInfoUnknown": "ukjend", "@viewerInfoUnknown": {}, @@ -436,7 +436,7 @@ "@viewerInfoLabelOwner": {}, "viewerInfoLabelCoordinates": "Koordinatar", "@viewerInfoLabelCoordinates": {}, - "tagEditorPageAddTagTooltip": "Legg til merkelapp", + "tagEditorPageAddTagTooltip": "Legg til merke", "@tagEditorPageAddTagTooltip": {}, "filePickerDoNotShowHiddenFiles": "Ikkje vis skjulte filer", "@filePickerDoNotShowHiddenFiles": {}, @@ -446,19 +446,19 @@ "@panoramaDisableSensorControl": {}, "filePickerOpenFrom": "Opne ifrå", "@filePickerOpenFrom": {}, - "filePickerNoItems": "Ingen element", + "filePickerNoItems": "Ingen ting", "@filePickerNoItems": {}, "nameConflictDialogSingleSourceMessage": "Somme filer i målmappa har same namn.", "@nameConflictDialogSingleSourceMessage": {}, "nameConflictDialogMultipleSourceMessage": "Somme filer har same namn.", "@nameConflictDialogMultipleSourceMessage": {}, - "binEntriesConfirmationDialogMessage": "{count, plural, =1{Flytt dette elementet til papirkorga?} other{Flytt desse {count} elementa til papirkorga?}}", + "binEntriesConfirmationDialogMessage": "{count, plural, =1{Flytt denne tingen til papirkorga?} other{Flytt desse {count} tinga til papirkorga?}}", "@binEntriesConfirmationDialogMessage": { "placeholders": { "count": {} } }, - "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Slett dette elementet?} other{Slett desse {count} elementa?}}", + "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Slett denne tingen?} other{Slett desse {count} tinga?}}", "@deleteEntriesConfirmationDialogMessage": { "placeholders": { "count": {} @@ -470,7 +470,7 @@ "@entryActionShareVideoOnly": {}, "entryActionShareImageOnly": "Del berre bilete", "@entryActionShareImageOnly": {}, - "unsupportedTypeDialogMessage": "{count, plural, other{Denne gjerda er ustødd for element av fylgjande slag: {types}.}}", + "unsupportedTypeDialogMessage": "{count, plural, other{Denne gjerda er ustødd for ting av fylgjande slag: {types}.}}", "@unsupportedTypeDialogMessage": { "placeholders": { "count": {}, @@ -481,17 +481,17 @@ } } }, - "addShortcutDialogLabel": "Snarvegsmerkelapp", + "addShortcutDialogLabel": "Snarvegsmerke", "@addShortcutDialogLabel": {}, "addShortcutButtonLabel": "LEGG TIL", "@addShortcutButtonLabel": {}, "noMatchingAppDialogMessage": "Ingen appar kan handsame dette.", "@noMatchingAppDialogMessage": {}, - "moveUndatedConfirmationDialogMessage": "Gøym elementdatoar før framhald?", + "moveUndatedConfirmationDialogMessage": "Spar datoane til tinga før du går vidare?", "@moveUndatedConfirmationDialogMessage": {}, - "moveUndatedConfirmationDialogSetDate": "Gøym datoar", + "moveUndatedConfirmationDialogSetDate": "Spar datoar", "@moveUndatedConfirmationDialogSetDate": {}, - "setCoverDialogLatest": "Nyaste element", + "setCoverDialogLatest": "Nyaste ting", "@setCoverDialogLatest": {}, "setCoverDialogAuto": "Auto", "@setCoverDialogAuto": {}, @@ -505,11 +505,11 @@ "@renameAlbumDialogLabel": {}, "renameAlbumDialogLabelAlreadyExistsHelper": "Mappa finst alt", "@renameAlbumDialogLabelAlreadyExistsHelper": {}, - "renameEntrySetPageTitle": "Døyp om", + "renameEntrySetPageTitle": "Byt namn", "@renameEntrySetPageTitle": {}, "exportEntryDialogWidth": "Breidd", "@exportEntryDialogWidth": {}, - "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Slett desse albuma og deira element?} other{Slett desse albuma og deira {count} element?}}", + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Slett desse albuma og deira ting?} other{Slett desse albuma og deira {count} ting?}}", "@deleteMultiAlbumConfirmationDialogMessage": { "placeholders": { "count": {} @@ -521,7 +521,7 @@ "@editEntryDateDialogExtractFromTitle": {}, "editEntryDateDialogCopyField": "Kopier ifrå annan dato", "@editEntryDateDialogCopyField": {}, - "editEntryDateDialogShift": "Byt", + "editEntryDateDialogShift": "Skyv", "@editEntryDateDialogShift": {}, "durationDialogSeconds": "Sekund", "@durationDialogSeconds": {}, @@ -563,9 +563,9 @@ "@viewerInfoSearchEmpty": {}, "viewerInfoSearchSuggestionDate": "Dato og tid", "@viewerInfoSearchSuggestionDate": {}, - "tagEditorPageTitle": "Brigd merkelappar", + "tagEditorPageTitle": "Brigd merke", "@tagEditorPageTitle": {}, - "tagEditorPageNewTagFieldLabel": "Ny merkelapp", + "tagEditorPageNewTagFieldLabel": "Nytt merke", "@tagEditorPageNewTagFieldLabel": {}, "filePickerShowHiddenFiles": "Vis skjulte filer", "@filePickerShowHiddenFiles": {}, @@ -613,9 +613,9 @@ "@viewerTransitionFade": {}, "widgetDisplayedItemRandom": "Tilfeldig", "@widgetDisplayedItemRandom": {}, - "widgetOpenPageHome": "Opne heimside", + "widgetOpenPageHome": "Opne heimsida", "@widgetOpenPageHome": {}, - "restrictedAccessDialogMessage": "Denne appen har ikkje lov til å brigde filer i «{directory}»-mappa i «{volume}».\n\nBruk ein førehandsinnlagd filhandsamar eller galleriapp til å flytta elementa til ei anna mappe.", + "restrictedAccessDialogMessage": "Denne appen har ikkje lov til å brigde filer i «{directory}»-mappa i «{volume}».\n\nBruk ein førehandsinnlagd filhandsamar eller galleriapp til å flytta tinga til ei anna mappe.", "@restrictedAccessDialogMessage": { "placeholders": { "directory": { @@ -629,7 +629,7 @@ } } }, - "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Slett dette albumet og elementet i det?} other{Slett dette albumet og dei {count} elementa i det?}}", + "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Slett dette albumet og tingen i det?} other{Slett dette albumet og dei {count} tinga i det?}}", "@deleteSingleAlbumConfirmationDialogMessage": { "placeholders": { "count": {} @@ -669,9 +669,9 @@ "@settingsUnitSystemDialogTitle": {}, "settingsCoordinateFormatTile": "Koordinatformat", "@settingsCoordinateFormatTile": {}, - "settingsLanguagePageTitle": "Mål", + "settingsLanguagePageTitle": "Skriftmål", "@settingsLanguagePageTitle": {}, - "settingsLanguageSectionTitle": "Mål og format", + "settingsLanguageSectionTitle": "Skriftmål og format", "@settingsLanguageSectionTitle": {}, "settingsDisplaySectionTitle": "Vising", "@settingsDisplaySectionTitle": {}, @@ -707,7 +707,7 @@ "@videoStreamSelectionDialogOff": {}, "videoStreamSelectionDialogNoSelection": "Det er ingen andre spor.", "@videoStreamSelectionDialogNoSelection": {}, - "genericSuccessFeedback": "Fullgjort", + "genericSuccessFeedback": "Ferdig", "@genericSuccessFeedback": {}, "genericFailureFeedback": "Mislykka", "@genericFailureFeedback": {}, @@ -749,6 +749,702 @@ "@aboutBugSectionTitle": {}, "aboutTranslatorsSectionTitle": "Omsetjarar", "@aboutTranslatorsSectionTitle": {}, - "viewerInfoOpenEmbeddedFailureFeedback": "Kunne ikkje ta ut innbygde opplysingar", - "@viewerInfoOpenEmbeddedFailureFeedback": {} + "viewerInfoOpenEmbeddedFailureFeedback": "Greidde ikkje å pakke ut innbygde data", + "@viewerInfoOpenEmbeddedFailureFeedback": {}, + "dateThisMonth": "Denne månaden", + "@dateThisMonth": {}, + "settingsViewerSectionTitle": "Visinga", + "@settingsViewerSectionTitle": {}, + "cropAspectRatioFree": "Frihand", + "@cropAspectRatioFree": {}, + "cropAspectRatioOriginal": "Opphavleg", + "@cropAspectRatioOriginal": {}, + "cropAspectRatioSquare": "Kvadrat", + "@cropAspectRatioSquare": {}, + "videoResumptionModeNever": "Aldri", + "@videoResumptionModeNever": {}, + "wallpaperTargetHome": "Heimskjermen", + "@wallpaperTargetHome": {}, + "settingsViewerShowMinimap": "Vis småkart", + "@settingsViewerShowMinimap": {}, + "patternDialogConfirm": "Stadfest mønsteret", + "@patternDialogConfirm": {}, + "exportEntryDialogWriteMetadata": "Tak med metadata", + "@exportEntryDialogWriteMetadata": {}, + "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP er turvt for å kunne spela av videoen inni rørslebilete.\n\nEr du viss på at du vil ta det bort?", + "@removeEntryMetadataMotionPhotoXmpWarningDialogMessage": {}, + "aboutBugSaveLogInstruction": "Spar apploggar til ei fil", + "@aboutBugSaveLogInstruction": {}, + "collectionActionMove": "Flytt til album", + "@collectionActionMove": {}, + "collectionGroupAlbum": "Etter album", + "@collectionGroupAlbum": {}, + "collectionGroupMonth": "Etter månad", + "@collectionGroupMonth": {}, + "sectionUnknown": "Ukjend", + "@sectionUnknown": {}, + "collectionEditFailureFeedback": "{count, plural, other{Kunne ikkje brigde {count} ting}}", + "@collectionEditFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionMoveSuccessFeedback": "{count, plural, other{Flytte {count} ting}}", + "@collectionMoveSuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionEmptyFavourites": "Det du likar vert vist her", + "@collectionEmptyFavourites": {}, + "collectionEmptyVideos": "Ingen videoar", + "@collectionEmptyVideos": {}, + "drawerCollectionMotionPhotos": "Rørslebilete", + "@drawerCollectionMotionPhotos": {}, + "drawerTagPage": "Merke", + "@drawerTagPage": {}, + "sortByDate": "Etter dato", + "@sortByDate": {}, + "sortBySize": "Etter storleik", + "@sortBySize": {}, + "sortByRating": "Etter omdøme", + "@sortByRating": {}, + "sortOrderNewestFirst": "Nyaste først", + "@sortOrderNewestFirst": {}, + "sortOrderOldestFirst": "Eldste først", + "@sortOrderOldestFirst": {}, + "sortOrderLargestFirst": "Største først", + "@sortOrderLargestFirst": {}, + "albumScreenRecordings": "Skjermopptak", + "@albumScreenRecordings": {}, + "albumVideoCaptures": "Videoopptak", + "@albumVideoCaptures": {}, + "albumEmpty": "Ingen album", + "@albumEmpty": {}, + "searchAlbumsSectionTitle": "Album", + "@searchAlbumsSectionTitle": {}, + "searchCountriesSectionTitle": "Land", + "@searchCountriesSectionTitle": {}, + "settingsConfirmationVaultDataLoss": "Åtvar om datatap ved kvelvet", + "@settingsConfirmationVaultDataLoss": {}, + "settingsNavigationDrawerAddAlbum": "Legg til album", + "@settingsNavigationDrawerAddAlbum": {}, + "settingsConfirmationAfterMoveToBinItems": "Verta spurd om du vil angre etter å ha kasta ting i søppelkorga", + "@settingsConfirmationAfterMoveToBinItems": {}, + "settingsThumbnailShowTagIcon": "Vis merke-ikon", + "@settingsThumbnailShowTagIcon": {}, + "settingsCollectionBurstPatternsNone": "Ingen", + "@settingsCollectionBurstPatternsNone": {}, + "settingsViewerQuickActionEmpty": "Ingen knappar", + "@settingsViewerQuickActionEmpty": {}, + "settingsViewerShowOverlayOnOpening": "Vis ved opning", + "@settingsViewerShowOverlayOnOpening": {}, + "settingsViewerShowShootingDetails": "Vis biletetakingsopplysingar", + "@settingsViewerShowShootingDetails": {}, + "settingsSlideshowFillScreen": "Fyll skjermen", + "@settingsSlideshowFillScreen": {}, + "settingsViewerEnableOverlayBlurEffect": "Uskarp verknad", + "@settingsViewerEnableOverlayBlurEffect": {}, + "settingsSlideshowVideoPlaybackTile": "Videoavspeling", + "@settingsSlideshowVideoPlaybackTile": {}, + "settingsSlideshowVideoPlaybackDialogTitle": "Videoavspeling", + "@settingsSlideshowVideoPlaybackDialogTitle": {}, + "settingsVideoShowVideos": "Vis videoar", + "@settingsVideoShowVideos": {}, + "settingsVideoEnableHardwareAcceleration": "Auk ytinga med maskinvara", + "@settingsVideoEnableHardwareAcceleration": {}, + "settingsVideoBackgroundMode": "Spel av i bakgrunnen", + "@settingsVideoBackgroundMode": {}, + "settingsVideoBackgroundModeDialogTitle": "Spel av i bakgrunnen", + "@settingsVideoBackgroundModeDialogTitle": {}, + "settingsSlideshowIntervalTile": "Byt til neste etter", + "@settingsSlideshowIntervalTile": {}, + "settingsSubtitleThemeTextAlignmentTile": "Tekststilling", + "@settingsSubtitleThemeTextAlignmentTile": {}, + "settingsSubtitleThemeShowOutline": "Vis omriss og skuggar", + "@settingsSubtitleThemeShowOutline": {}, + "settingsSubtitleThemeTextAlignmentDialogTitle": "Tekststilling", + "@settingsSubtitleThemeTextAlignmentDialogTitle": {}, + "settingsAccessibilityShowPinchGestureAlternatives": "Vis andre utvegar for fleirtrykkhandvendingar", + "@settingsAccessibilityShowPinchGestureAlternatives": {}, + "settingsThemeEnableDynamicColor": "Skiftande let", + "@settingsThemeEnableDynamicColor": {}, + "settingsHiddenItemsTile": "Skjulte ting", + "@settingsHiddenItemsTile": {}, + "settingsStorageAccessBanner": "Nokre mapper krev at du gjev appen tilgjenge til dei for å brigde filer i dei. Du kan sjå på mappene som du har gjeve appen tilgjenge til her.", + "@settingsStorageAccessBanner": {}, + "settingsDisplayRefreshRateModeDialogTitle": "Rørslejamleik", + "@settingsDisplayRefreshRateModeDialogTitle": {}, + "viewerInfoPageTitle": "Opplysingar", + "@viewerInfoPageTitle": {}, + "mapPointNorthUpTooltip": "Rett opp", + "@mapPointNorthUpTooltip": {}, + "setCoverDialogCustom": "Eiget", + "@setCoverDialogCustom": {}, + "pinDialogEnter": "Skriv inn ein talkode", + "@pinDialogEnter": {}, + "settingsNavigationDrawerBanner": "Trykk og hald for å flytta og brigde rekkjefylgda til menytinga.", + "@settingsNavigationDrawerBanner": {}, + "settingsEnableBinSubtitle": "Sparar på sletta ting i 30 dagar", + "@settingsEnableBinSubtitle": {}, + "settingsHiddenItemsTabPaths": "Skjulte mapper", + "@settingsHiddenItemsTabPaths": {}, + "statsTopStatesSectionTitle": "Topplandsdelar", + "@statsTopStatesSectionTitle": {}, + "chipActionCreateVault": "Kvelv", + "@chipActionCreateVault": {}, + "chipActionConfigureVault": "Set opp kvelvet", + "@chipActionConfigureVault": {}, + "displayRefreshRatePreferLowest": "Mest ujamn", + "@displayRefreshRatePreferLowest": {}, + "filterTaggedLabel": "Merkte", + "@filterTaggedLabel": {}, + "displayRefreshRatePreferHighest": "Mest jamn", + "@displayRefreshRatePreferHighest": {}, + "settingsThumbnailShowLocationIcon": "Vis stad-ikon", + "@settingsThumbnailShowLocationIcon": {}, + "settingsThumbnailShowFavouriteIcon": "Vis likt-ikon", + "@settingsThumbnailShowFavouriteIcon": {}, + "settingsImageBackground": "Biletbakgrunn", + "@settingsImageBackground": {}, + "settingsViewerQuickActionsTile": "Kvikkgjerder", + "@settingsViewerQuickActionsTile": {}, + "settingsViewerShowRatingTags": "Vis omdøme og merke", + "@settingsViewerShowRatingTags": {}, + "settingsViewerQuickActionEditorAvailableButtonsSectionTitle": "Tilgjengelege knappar", + "@settingsViewerQuickActionEditorAvailableButtonsSectionTitle": {}, + "saveCopyButtonLabel": "SPAR KOPI", + "@saveCopyButtonLabel": {}, + "applyTooltip": "Nytt", + "@applyTooltip": {}, + "editorActionTransform": "Form om", + "@editorActionTransform": {}, + "editorTransformCrop": "Klipp til", + "@editorTransformCrop": {}, + "editorTransformRotate": "Snu", + "@editorTransformRotate": {}, + "maxBrightnessNever": "Aldri", + "@maxBrightnessNever": {}, + "wallpaperTargetHomeLock": "Heim- og låseskjermane", + "@wallpaperTargetHomeLock": {}, + "sortOrderAtoZ": "A-Å", + "@sortOrderAtoZ": {}, + "createAlbumButtonLabel": "LAG", + "@createAlbumButtonLabel": {}, + "countryPageTitle": "Land", + "@countryPageTitle": {}, + "countryEmpty": "Ingen land", + "@countryEmpty": {}, + "tagEmpty": "Ingen merke", + "@tagEmpty": {}, + "newVaultWarningDialogMessage": "Ting i kvelva er berre tilgjengelege for denne appen.\n\nOm du slettar denne appen eller tømmer dataa til denne appen, vil du missa alt som er i kvelva.", + "@newVaultWarningDialogMessage": {}, + "newVaultDialogTitle": "Nytt kvelv", + "@newVaultDialogTitle": {}, + "configureVaultDialogTitle": "Set opp kvelvet", + "@configureVaultDialogTitle": {}, + "vaultDialogLockModeWhenScreenOff": "Lås når skjermen slår seg av", + "@vaultDialogLockModeWhenScreenOff": {}, + "sortOrderLowestFirst": "Lægste først", + "@sortOrderLowestFirst": {}, + "maxBrightnessAlways": "Heile tida", + "@maxBrightnessAlways": {}, + "videoResumptionModeAlways": "Kvar gong", + "@videoResumptionModeAlways": {}, + "vaultDialogLockTypeLabel": "Låseslag", + "@vaultDialogLockTypeLabel": {}, + "pinDialogConfirm": "Stadfest talkoden", + "@pinDialogConfirm": {}, + "passwordDialogEnter": "Skriv inn passordet", + "@passwordDialogEnter": {}, + "vaultBinUsageDialogMessage": "Nokre kvelv nyttar papirkorga.", + "@vaultBinUsageDialogMessage": {}, + "mapEmptyRegion": "Ingen bilete i dette området", + "@mapEmptyRegion": {}, + "exportEntryDialogQuality": "Kvalitet", + "@exportEntryDialogQuality": {}, + "tooManyItemsErrorDialogMessage": "Røyn att med færre ting.", + "@tooManyItemsErrorDialogMessage": {}, + "aboutLicensesShowAllButtonLabel": "Vis alle løyve", + "@aboutLicensesShowAllButtonLabel": {}, + "collectionActionEdit": "Brigd", + "@collectionActionEdit": {}, + "collectionSearchTitlesHintText": "Søk etter namn", + "@collectionSearchTitlesHintText": {}, + "collectionGroupDay": "Etter dag", + "@collectionGroupDay": {}, + "collectionMoveFailureFeedback": "{count, plural, other{Kunne ikkje flytta {count} ting}}", + "@collectionMoveFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionRenameFailureFeedback": "{count, plural, other{Kunne ikkje byta namn på {count} ting}}", + "@collectionRenameFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "settingsAskEverytime": "Spør kvar gong", + "@settingsAskEverytime": {}, + "settingsThumbnailShowRating": "Vis omdøme", + "@settingsThumbnailShowRating": {}, + "settingsThumbnailShowRawIcon": "Vis rå-ikon", + "@settingsThumbnailShowRawIcon": {}, + "settingsCollectionQuickActionEditorPageTitle": "Kvikkgjerder", + "@settingsCollectionQuickActionEditorPageTitle": {}, + "settingsCollectionSelectionQuickActionEditorBanner": "Trykk og hald for å flytta knappane og velja kva for slags gjerder som skal visast når du vel ting.", + "@settingsCollectionSelectionQuickActionEditorBanner": {}, + "settingsCollectionQuickActionTabBrowsing": "Gjennomsyn", + "@settingsCollectionQuickActionTabBrowsing": {}, + "settingsThumbnailShowMotionPhotoIcon": "Vis rørslebilete-ikon", + "@settingsThumbnailShowMotionPhotoIcon": {}, + "settingsViewerQuickActionEditorBanner": "Trykk og hald for å flytta knappane og velja kva for slags gjerder som skal visast i visinga.", + "@settingsViewerQuickActionEditorBanner": {}, + "settingsViewerSlideshowTile": "Ljosbiletevising", + "@settingsViewerSlideshowTile": {}, + "settingsViewerMaximumBrightness": "Full ljosstyrke", + "@settingsViewerMaximumBrightness": {}, + "settingsViewerUseCutout": "Nytt utklipt område", + "@settingsViewerUseCutout": {}, + "settingsVideoPlaybackTile": "Avspeling", + "@settingsVideoPlaybackTile": {}, + "settingsVideoPlaybackPageTitle": "Avspeling", + "@settingsVideoPlaybackPageTitle": {}, + "settingsVideoLoopModeDialogTitle": "Gjentak", + "@settingsVideoLoopModeDialogTitle": {}, + "settingsVideoResumptionModeTile": "Spel av der du slapp", + "@settingsVideoResumptionModeTile": {}, + "settingsVideoResumptionModeDialogTitle": "Spel av der du slapp", + "@settingsVideoResumptionModeDialogTitle": {}, + "settingsVideoControlsPageTitle": "Styring", + "@settingsVideoControlsPageTitle": {}, + "settingsVideoGestureDoubleTapTogglePlay": "Dobbeltrykk for å spela/stansa", + "@settingsVideoGestureDoubleTapTogglePlay": {}, + "settingsVideoGestureVerticalDragBrightnessVolume": "Dra opp eller ned for å brigde ljos-/ljodstyrken", + "@settingsVideoGestureVerticalDragBrightnessVolume": {}, + "settingsVideoButtonsTile": "Knappar", + "@settingsVideoButtonsTile": {}, + "settingsSubtitleThemeTextPositionDialogTitle": "Tekststad", + "@settingsSubtitleThemeTextPositionDialogTitle": {}, + "settingsSubtitleThemeTextSize": "Tekststorleik", + "@settingsSubtitleThemeTextSize": {}, + "settingsHiddenPathsBanner": "Bilete og videoar i desse mappene, og undermappene deiras, vert ikkje viste i samlinga di.", + "@settingsHiddenPathsBanner": {}, + "settingsAccessibilitySectionTitle": "Tilgjenge", + "@settingsAccessibilitySectionTitle": {}, + "settingsStorageAccessRevokeTooltip": "Fråta", + "@settingsStorageAccessRevokeTooltip": {}, + "settingsStorageAccessEmpty": "Ingen gjevne tilgjenge", + "@settingsStorageAccessEmpty": {}, + "settingsDisplayRefreshRateModeTile": "Rørslejamleiken til skjermen", + "@settingsDisplayRefreshRateModeTile": {}, + "tagEditorDiscardDialogMessage": "Vil du avvisa brigda?", + "@tagEditorDiscardDialogMessage": {}, + "sortByName": "Etter namn", + "@sortByName": {}, + "sortByItemCount": "Etter mengd ting", + "@sortByItemCount": {}, + "albumGroupVolume": "Etter gøymestad", + "@albumGroupVolume": {}, + "searchTagsSectionTitle": "Merke", + "@searchTagsSectionTitle": {}, + "settingsSearchFieldLabel": "Søkjeinnstillingar", + "@settingsSearchFieldLabel": {}, + "settingsKeepScreenOnDialogTitle": "Hald skjermen slegen på", + "@settingsKeepScreenOnDialogTitle": {}, + "settingsDoubleBackExit": "Trykk «Attende» to gongar for å gå ut av appen", + "@settingsDoubleBackExit": {}, + "settingsNavigationDrawerTabPages": "Sider", + "@settingsNavigationDrawerTabPages": {}, + "settingsCollectionQuickActionsTile": "Kvikkgjerder", + "@settingsCollectionQuickActionsTile": {}, + "settingsThumbnailShowVideoDuration": "Vis videolengd", + "@settingsThumbnailShowVideoDuration": {}, + "settingsViewerGestureSideTapNext": "Trykk på skjermkantane for å visa førre/neste ting", + "@settingsViewerGestureSideTapNext": {}, + "settingsViewerQuickActionEditorPageTitle": "Kvikkgjerder", + "@settingsViewerQuickActionEditorPageTitle": {}, + "settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": "Viste knappar", + "@settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": {}, + "settingsViewerOverlayTile": "Overlag", + "@settingsViewerOverlayTile": {}, + "settingsSlideshowTransitionTile": "Overgang", + "@settingsSlideshowTransitionTile": {}, + "settingsStorageAccessPageTitle": "Gøymetilgjenge", + "@settingsStorageAccessPageTitle": {}, + "settingsDisplayUseTvInterface": "Android TV-grensesnitt", + "@settingsDisplayUseTvInterface": {}, + "settingsActionExport": "Før ut", + "@settingsActionExport": {}, + "settingsNavigationDrawerTabAlbums": "Album", + "@settingsNavigationDrawerTabAlbums": {}, + "settingsThumbnailOverlayPageTitle": "Overlag", + "@settingsThumbnailOverlayPageTitle": {}, + "editEntryDateDialogSetCustom": "Vel dato å setja", + "@editEntryDateDialogSetCustom": {}, + "aboutBugReportButton": "Rapporter", + "@aboutBugReportButton": {}, + "aboutLicensesSectionTitle": "Løyve til opne kjeldekodar", + "@aboutLicensesSectionTitle": {}, + "aboutLicensesFlutterPluginsSectionTitle": "Flutter-tillegg", + "@aboutLicensesFlutterPluginsSectionTitle": {}, + "collectionActionEmptyBin": "Tøm papirkorga", + "@collectionActionEmptyBin": {}, + "collectionActionCopy": "Kopier til album", + "@collectionActionCopy": {}, + "collectionCopySuccessFeedback": "{count, plural, other{Kopierte {count} ting}}", + "@collectionCopySuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "drawerAboutButton": "Om", + "@drawerAboutButton": {}, + "collectionEmptyImages": "Ingen bilete", + "@collectionEmptyImages": {}, + "collectionEmptyGrantAccessButtonLabel": "Gje tilgjenge", + "@collectionEmptyGrantAccessButtonLabel": {}, + "drawerCollectionVideos": "Videoar", + "@drawerCollectionVideos": {}, + "drawerCountryPage": "Land", + "@drawerCountryPage": {}, + "sortOrderZtoA": "Å-A", + "@sortOrderZtoA": {}, + "sortOrderHighestFirst": "Høgste først", + "@sortOrderHighestFirst": {}, + "albumMimeTypeMixed": "Blanda", + "@albumMimeTypeMixed": {}, + "albumPickPageTitlePick": "Vel album", + "@albumPickPageTitlePick": {}, + "albumScreenshots": "Skjermbilete", + "@albumScreenshots": {}, + "settingsHomeTile": "Heim", + "@settingsHomeTile": {}, + "settingsViewerOverlayPageTitle": "Overlag", + "@settingsViewerOverlayPageTitle": {}, + "settingsViewerSlideshowPageTitle": "Ljosbiletevising", + "@settingsViewerSlideshowPageTitle": {}, + "settingsSlideshowRepeat": "Gjentak", + "@settingsSlideshowRepeat": {}, + "settingsVideoPageTitle": "Videoinnstillingar", + "@settingsVideoPageTitle": {}, + "settingsVideoSectionTitle": "Video", + "@settingsVideoSectionTitle": {}, + "settingsSubtitleThemeTile": "Undertekster", + "@settingsSubtitleThemeTile": {}, + "settingsSubtitleThemePageTitle": "Undertekster", + "@settingsSubtitleThemePageTitle": {}, + "settingsSubtitleThemeSample": "Dette er eit døme.", + "@settingsSubtitleThemeSample": {}, + "settingsSubtitleThemeTextPositionTile": "Tekststad", + "@settingsSubtitleThemeTextPositionTile": {}, + "settingsSubtitleThemeTextAlignmentLeft": "Venstre", + "@settingsSubtitleThemeTextAlignmentLeft": {}, + "settingsSubtitleThemeBackgroundOpacity": "Bakgrunnsgjennomsyn", + "@settingsSubtitleThemeBackgroundOpacity": {}, + "settingsAllowInstalledAppAccess": "Gje tilgjenge til lista over innlagde appar", + "@settingsAllowInstalledAppAccess": {}, + "settingsAllowInstalledAppAccessSubtitle": "Vert nytta til å betre albumsvisinga", + "@settingsAllowInstalledAppAccessSubtitle": {}, + "settingsSaveSearchHistory": "Spar søkjehistorikken", + "@settingsSaveSearchHistory": {}, + "settingsAllowErrorReporting": "Send inn anonyme feilrapportar", + "@settingsAllowErrorReporting": {}, + "collectionSelectSectionTooltip": "Vel utvalet", + "@collectionSelectSectionTooltip": {}, + "settingsCollectionBrowsingQuickActionEditorBanner": "Trykk og hald for å flytta knappane og velja kva for slags gjerder som skal visast når du ser igjennom ting.", + "@settingsCollectionBrowsingQuickActionEditorBanner": {}, + "settingsMotionPhotoAutoPlay": "Spel av rørslebilete sjølvverkande", + "@settingsMotionPhotoAutoPlay": {}, + "settingsViewerShowDescription": "Vis utgreiingar", + "@settingsViewerShowDescription": {}, + "settingsViewerShowInformationSubtitle": "Vis namn, dato, stad, osv …", + "@settingsViewerShowInformationSubtitle": {}, + "settingsThemeBrightnessDialogTitle": "Ham", + "@settingsThemeBrightnessDialogTitle": {}, + "settingsThemeBrightnessTile": "Ham", + "@settingsThemeBrightnessTile": {}, + "viewerInfoLabelDescription": "Utgreiing", + "@viewerInfoLabelDescription": {}, + "passwordDialogConfirm": "Stadfest passordet", + "@passwordDialogConfirm": {}, + "drawerCollectionImages": "Bilete", + "@drawerCollectionImages": {}, + "settingsConfirmationDialogTitle": "Stadfestingsvindaugo", + "@settingsConfirmationDialogTitle": {}, + "settingsConfirmationBeforeDeleteItems": "Verta spurd før du slettar noko for godt", + "@settingsConfirmationBeforeDeleteItems": {}, + "settingsStorageAccessTile": "Gøymetilgjenge", + "@settingsStorageAccessTile": {}, + "tagPlaceholderState": "Landsdel", + "@tagPlaceholderState": {}, + "chipActionShowCountryStates": "Vis landsdelar", + "@chipActionShowCountryStates": {}, + "viewerActionLock": "Lås visinga", + "@viewerActionLock": {}, + "viewerActionUnlock": "Lås opp visinga", + "@viewerActionUnlock": {}, + "albumTierVaults": "Kvelv", + "@albumTierVaults": {}, + "lengthUnitPercent": "%", + "@lengthUnitPercent": {}, + "collectionPickPageTitle": "Vel", + "@collectionPickPageTitle": {}, + "collectionSelectPageTitle": "Vel ting", + "@collectionSelectPageTitle": {}, + "collectionActionAddShortcut": "Legg til snarveg", + "@collectionActionAddShortcut": {}, + "collectionDeleteFailureFeedback": "{count, plural, other{Kunne ikkje slette {count} ting}}", + "@collectionDeleteFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionCopyFailureFeedback": "{count, plural, other{Kunne ikkje kopiera {count} ting}}", + "@collectionCopyFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionEditSuccessFeedback": "{count, plural, other{Brigda {count} ting}}", + "@collectionEditSuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "drawerSettingsButton": "Innstillingar", + "@drawerSettingsButton": {}, + "collectionDeselectSectionTooltip": "Fråvel utvalet", + "@collectionDeselectSectionTooltip": {}, + "drawerCollectionAll": "Alle samlingar", + "@drawerCollectionAll": {}, + "sortOrderSmallestFirst": "Smæste først", + "@sortOrderSmallestFirst": {}, + "albumPickPageTitleCopy": "Kopier til album", + "@albumPickPageTitleCopy": {}, + "drawerCollectionFavourites": "Likte", + "@drawerCollectionFavourites": {}, + "albumGroupTier": "Etter nivå", + "@albumGroupTier": {}, + "albumPickPageTitleMove": "Flytt til album", + "@albumPickPageTitleMove": {}, + "albumGroupType": "Etter slag", + "@albumGroupType": {}, + "albumCamera": "Kamera", + "@albumCamera": {}, + "albumDownload": "Hent", + "@albumDownload": {}, + "statePageTitle": "Landsdelar", + "@statePageTitle": {}, + "stateEmpty": "Ingen landsdelar", + "@stateEmpty": {}, + "placePageTitle": "Stadar", + "@placePageTitle": {}, + "placeEmpty": "Ingen stadar", + "@placeEmpty": {}, + "tagPageTitle": "Merke", + "@tagPageTitle": {}, + "binPageTitle": "Papirkorga", + "@binPageTitle": {}, + "searchCollectionFieldHint": "Søk i samlinga", + "@searchCollectionFieldHint": {}, + "addPathTooltip": "Legg til mappe", + "@addPathTooltip": {}, + "settingsTimeToTakeActionTile": "Tid for å gjera", + "@settingsTimeToTakeActionTile": {}, + "searchStatesSectionTitle": "Landsdelar", + "@searchStatesSectionTitle": {}, + "searchPlacesSectionTitle": "Stadar", + "@searchPlacesSectionTitle": {}, + "searchRatingSectionTitle": "Omdøme", + "@searchRatingSectionTitle": {}, + "searchMetadataSectionTitle": "Metadata", + "@searchMetadataSectionTitle": {}, + "settingsPageTitle": "Innstillingar", + "@settingsPageTitle": {}, + "settingsSystemDefault": "Systemforval", + "@settingsSystemDefault": {}, + "settingsDefault": "Forval", + "@settingsDefault": {}, + "settingsDisabled": "Slegen av", + "@settingsDisabled": {}, + "settingsModificationWarningDialogMessage": "Andre innstillingar vil brigdast.", + "@settingsModificationWarningDialogMessage": {}, + "settingsSearchEmpty": "Ingen samsvarande innstilling", + "@settingsSearchEmpty": {}, + "settingsActionExportDialogTitle": "Før ut", + "@settingsActionExportDialogTitle": {}, + "settingsActionImport": "Før inn", + "@settingsActionImport": {}, + "settingsActionImportDialogTitle": "Før inn", + "@settingsActionImportDialogTitle": {}, + "appExportCovers": "Omslag", + "@appExportCovers": {}, + "appExportFavourites": "Likte ting", + "@appExportFavourites": {}, + "appExportSettings": "Innstillingar", + "@appExportSettings": {}, + "settingsHomeDialogTitle": "Heim", + "@settingsHomeDialogTitle": {}, + "settingsConfirmationTile": "Stadfestingsvindaugo", + "@settingsConfirmationTile": {}, + "settingsVideoGestureSideDoubleTapSeek": "Dobbeltrykk på skjermkantane for å hoppe framover/bakover", + "@settingsVideoGestureSideDoubleTapSeek": {}, + "settingsSubtitleThemeTextColor": "Tekstleten", + "@settingsSubtitleThemeTextColor": {}, + "settingsSubtitleThemeTextAlignmentRight": "Høgre", + "@settingsSubtitleThemeTextAlignmentRight": {}, + "settingsPrivacySectionTitle": "Personvern", + "@settingsPrivacySectionTitle": {}, + "settingsDisablingBinWarningDialogMessage": "Ting i papirkorga vil slettast for godt.", + "@settingsDisablingBinWarningDialogMessage": {}, + "settingsAllowMediaManagement": "La få handsame medium", + "@settingsAllowMediaManagement": {}, + "settingsHiddenItemsPageTitle": "Skjulte ting", + "@settingsHiddenItemsPageTitle": {}, + "lengthUnitPixel": "px", + "@lengthUnitPixel": {}, + "vaultLockTypePin": "Talkode", + "@vaultLockTypePin": {}, + "searchRecentSectionTitle": "Nylege", + "@searchRecentSectionTitle": {}, + "searchDateSectionTitle": "Dato", + "@searchDateSectionTitle": {}, + "settingsConfirmationBeforeMoveUndatedItems": "Verta spurd før du flyttar på ting utan dato", + "@settingsConfirmationBeforeMoveUndatedItems": {}, + "settingsEnableBin": "Nytt papirkorga", + "@settingsEnableBin": {}, + "chipActionLock": "Lås", + "@chipActionLock": {}, + "sortByAlbumFileName": "Etter album og filnamn", + "@sortByAlbumFileName": {}, + "aboutBugReportInstruction": "Rapporter på GitHub med loggføringene og systemopplysingane dine", + "@aboutBugReportInstruction": {}, + "aboutLinkPolicy": "Personvernutsegn", + "@aboutLinkPolicy": {}, + "collectionPageTitle": "Samling", + "@collectionPageTitle": {}, + "collectionRenameSuccessFeedback": "{count, plural, =1{Bytte namnet til 1 ting} other{Bytte namna til {count} ting}}", + "@collectionRenameSuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "drawerCollectionPanoramas": "Panorama", + "@drawerCollectionPanoramas": {}, + "drawerCollectionRaws": "Rå bilete", + "@drawerCollectionRaws": {}, + "drawerCollectionSphericalVideos": "360°-videoar", + "@drawerCollectionSphericalVideos": {}, + "drawerAlbumPage": "Album", + "@drawerAlbumPage": {}, + "drawerPlacePage": "Stadar", + "@drawerPlacePage": {}, + "albumPickPageTitleExport": "Før ut til album", + "@albumPickPageTitleExport": {}, + "albumPageTitle": "Album", + "@albumPageTitle": {}, + "newFilterBanner": "ny", + "@newFilterBanner": {}, + "settingsKeepScreenOnTile": "Hald skjermen slegen på", + "@settingsKeepScreenOnTile": {}, + "settingsThumbnailSectionTitle": "Småbilete", + "@settingsThumbnailSectionTitle": {}, + "settingsConfirmationBeforeMoveToBinItems": "Verta spurd før du kastar ting i papirkorga", + "@settingsConfirmationBeforeMoveToBinItems": {}, + "settingsNavigationDrawerTabTypes": "Slag", + "@settingsNavigationDrawerTabTypes": {}, + "settingsThumbnailOverlayTile": "Overlag", + "@settingsThumbnailOverlayTile": {}, + "settingsSubtitleThemeTextOpacity": "Tekstgjennomsyn", + "@settingsSubtitleThemeTextOpacity": {}, + "settingsSubtitleThemeBackgroundColor": "Bakgrunnsleten", + "@settingsSubtitleThemeBackgroundColor": {}, + "settingsSubtitleThemeTextAlignmentCenter": "Midten", + "@settingsSubtitleThemeTextAlignmentCenter": {}, + "settingsVideoControlsTile": "Styring", + "@settingsVideoControlsTile": {}, + "settingsViewerShowInformation": "Vis opplysingar", + "@settingsViewerShowInformation": {}, + "settingsViewerShowOverlayThumbnails": "Vis småbilete", + "@settingsViewerShowOverlayThumbnails": {}, + "settingsSlideshowShuffle": "Bland", + "@settingsSlideshowShuffle": {}, + "settingsVideoLoopModeTile": "Gjentak", + "@settingsVideoLoopModeTile": {}, + "settingsVideoAutoPlay": "Spel av med ein gong", + "@settingsVideoAutoPlay": {}, + "vaultLockTypePattern": "Mønster", + "@vaultLockTypePattern": {}, + "vaultLockTypePassword": "Passord", + "@vaultLockTypePassword": {}, + "patternDialogEnter": "Stryk inn eit mønster", + "@patternDialogEnter": {}, + "editEntryDialogTargetFieldsHeader": "Område å brigde", + "@editEntryDialogTargetFieldsHeader": {}, + "settingsCollectionQuickActionTabSelecting": "Veljing", + "@settingsCollectionQuickActionTabSelecting": {}, + "policyPageTitle": "Personvernutsegn", + "@policyPageTitle": {}, + "collectionActionRescan": "Gjennomsøk att", + "@collectionActionRescan": {}, + "dateToday": "I dag", + "@dateToday": {}, + "dateYesterday": "I går", + "@dateYesterday": {}, + "entryInfoActionRemoveLocation": "Ta bort stadsetjinga", + "@entryInfoActionRemoveLocation": {}, + "filterLocatedLabel": "Stadsette", + "@filterLocatedLabel": {}, + "settingsShowBottomNavigationBar": "Vis ei framfinningsrad på botnen", + "@settingsShowBottomNavigationBar": {}, + "settingsCollectionBurstPatternsTile": "Attkjenning av seriebilete", + "@settingsCollectionBurstPatternsTile": {}, + "chipActionGoToPlacePage": "Vis i Stadar", + "@chipActionGoToPlacePage": {}, + "settingsNavigationDrawerTile": "Framfinningsmenyen", + "@settingsNavigationDrawerTile": {}, + "settingsNavigationDrawerEditorPageTitle": "Framfinningsmenyen", + "@settingsNavigationDrawerEditorPageTitle": {}, + "locationPickerUseThisLocationButton": "Nytt denne stadsetjinga", + "@locationPickerUseThisLocationButton": {}, + "aboutLicensesFlutterPackagesSectionTitle": "Flutter-pakker", + "@aboutLicensesFlutterPackagesSectionTitle": {}, + "aboutLicensesDartPackagesSectionTitle": "Dart-pakker", + "@aboutLicensesDartPackagesSectionTitle": {}, + "statsWithGps": "{count, plural, other{{count} ting med stadsetjing}}", + "@statsWithGps": { + "placeholders": { + "count": {} + } + }, + "mapAttributionOsmHot": "Kartdata © [OpenStreetMap](https://www.openstreetmap.org/copyright)-medverkarar • Fliser av [HOT](https://www.hotosm.org/) • Hust av [OSM France](https://openstreetmap.fr/)", + "@mapAttributionOsmHot": {}, + "mapAttributionStamen": "Kartdata © [OpenStreetMap](https://www.openstreetmap.org/copyright)-medverkarar • Fliser av [Stamen Design](https://stamen.com), [CC BY 3.0](https://creativecommons.org/licenses/by/3.0)", + "@mapAttributionStamen": {}, + "columnCount": "{count, plural, =1{1 kolonne} other{{count} kolonnar}}", + "@columnCount": { + "placeholders": { + "count": {} + } + }, + "editEntryLocationDialogTitle": "Stadsetjing", + "@editEntryLocationDialogTitle": {}, + "collectionExportFailureFeedback": "{count, plural, =1{Greidde ikkje å føra ut 1 side} other{Greidde ikkje å føra ut {count} sider}}", + "@collectionExportFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "albumGroupNone": "Ikkje hop", + "@albumGroupNone": {}, + "editEntryLocationDialogSetCustom": "Set eiga stadsetjing", + "@editEntryLocationDialogSetCustom": {}, + "collectionGroupNone": "Ikkje hop", + "@collectionGroupNone": {}, + "filterNoLocationLabel": "Ustadsette", + "@filterNoLocationLabel": {}, + "settingsNavigationSectionTitle": "Finn fram", + "@settingsNavigationSectionTitle": {} } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 946d6043b..5a8cc2ea0 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1474,5 +1474,23 @@ "videoResumptionModeAlways": "Zawsze", "@videoResumptionModeAlways": {}, "exportEntryDialogQuality": "Jakość", - "@exportEntryDialogQuality": {} + "@exportEntryDialogQuality": {}, + "saveCopyButtonLabel": "ZAPISZ KOPIĘ", + "@saveCopyButtonLabel": {}, + "applyTooltip": "Zastosuj", + "@applyTooltip": {}, + "editorTransformRotate": "Obróć", + "@editorTransformRotate": {}, + "cropAspectRatioFree": "Dowolnie", + "@cropAspectRatioFree": {}, + "cropAspectRatioOriginal": "Oryginał", + "@cropAspectRatioOriginal": {}, + "editorActionTransform": "Przekształć", + "@editorActionTransform": {}, + "editorTransformCrop": "Przytnij", + "@editorTransformCrop": {}, + "cropAspectRatioSquare": "Kwadrat", + "@cropAspectRatioSquare": {}, + "widgetTapUpdateWidget": "Zaktualizuj widżet", + "@widgetTapUpdateWidget": {} } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 7e379a006..e695fe3ec 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1294,5 +1294,27 @@ "viewerActionUnlock": "Desbloquear visualizador", "@viewerActionUnlock": {}, "settingsVideoBackgroundModeDialogTitle": "Modo de fundo", - "@settingsVideoBackgroundModeDialogTitle": {} + "@settingsVideoBackgroundModeDialogTitle": {}, + "settingsVideoPlaybackTile": "Reproduzir", + "@settingsVideoPlaybackTile": {}, + "maxBrightnessAlways": "Sempre", + "@maxBrightnessAlways": {}, + "videoResumptionModeAlways": "Sempre", + "@videoResumptionModeAlways": {}, + "exportEntryDialogQuality": "Qualidade", + "@exportEntryDialogQuality": {}, + "settingsAskEverytime": "Perguntar sempre", + "@settingsAskEverytime": {}, + "settingsVideoPlaybackPageTitle": "Reproduzir", + "@settingsVideoPlaybackPageTitle": {}, + "settingsVideoResumptionModeTile": "Retomar a reprodução", + "@settingsVideoResumptionModeTile": {}, + "settingsVideoResumptionModeDialogTitle": "Retomar a reprodução", + "@settingsVideoResumptionModeDialogTitle": {}, + "maxBrightnessNever": "Nunca", + "@maxBrightnessNever": {}, + "videoResumptionModeNever": "Nunca", + "@videoResumptionModeNever": {}, + "tagEditorDiscardDialogMessage": "Pretende rejeitar as alterações?", + "@tagEditorDiscardDialogMessage": {} } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index a09e87d79..8ff4ec6f2 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -332,7 +332,7 @@ "@noMatchingAppDialogMessage": {}, "moveUndatedConfirmationDialogSetDate": "Зберегти дати", "@moveUndatedConfirmationDialogSetDate": {}, - "videoResumeDialogMessage": "Чи хочете ви відновити відтворення на {time}?", + "videoResumeDialogMessage": "Ви хочете продовжити відтворення з {time}?", "@videoResumeDialogMessage": { "placeholders": { "time": { @@ -1452,5 +1452,45 @@ "searchStatesSectionTitle": "Штати", "@searchStatesSectionTitle": {}, "statePageTitle": "Штати", - "@statePageTitle": {} + "@statePageTitle": {}, + "settingsAskEverytime": "Запитувати щоразу", + "@settingsAskEverytime": {}, + "maxBrightnessNever": "Ніколи", + "@maxBrightnessNever": {}, + "maxBrightnessAlways": "Завжди", + "@maxBrightnessAlways": {}, + "videoResumptionModeNever": "Ніколи", + "@videoResumptionModeNever": {}, + "exportEntryDialogQuality": "Якість", + "@exportEntryDialogQuality": {}, + "settingsVideoPlaybackTile": "Відтворення", + "@settingsVideoPlaybackTile": {}, + "settingsVideoPlaybackPageTitle": "Відтворення", + "@settingsVideoPlaybackPageTitle": {}, + "settingsVideoResumptionModeTile": "Продовжити відтворення", + "@settingsVideoResumptionModeTile": {}, + "settingsVideoResumptionModeDialogTitle": "Продовжити відтворення", + "@settingsVideoResumptionModeDialogTitle": {}, + "tagEditorDiscardDialogMessage": "Ви хочете відмовитися від змін?", + "@tagEditorDiscardDialogMessage": {}, + "videoResumptionModeAlways": "Завжди", + "@videoResumptionModeAlways": {}, + "applyTooltip": "Застосувати", + "@applyTooltip": {}, + "saveCopyButtonLabel": "ЗБЕРЕГТИ КОПІЮ", + "@saveCopyButtonLabel": {}, + "editorActionTransform": "Перетворити", + "@editorActionTransform": {}, + "editorTransformCrop": "Обрізати", + "@editorTransformCrop": {}, + "editorTransformRotate": "Повернути", + "@editorTransformRotate": {}, + "cropAspectRatioFree": "Без змін", + "@cropAspectRatioFree": {}, + "cropAspectRatioOriginal": "Оригінал", + "@cropAspectRatioOriginal": {}, + "cropAspectRatioSquare": "Площа", + "@cropAspectRatioSquare": {}, + "widgetTapUpdateWidget": "Оновити віджет", + "@widgetTapUpdateWidget": {} } diff --git a/lib/main_common.dart b/lib/main_common.dart index a02f671be..f525bab0d 100644 --- a/lib/main_common.dart +++ b/lib/main_common.dart @@ -5,7 +5,7 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:flutter/material.dart'; -void mainCommon(AppFlavor flavor) { +void mainCommon(AppFlavor flavor, {Map? debugIntentData}) { // HttpClient.enableTimelineLogging = true; // enable network traffic logging // debugPrintGestureArenaDiagnostics = true; @@ -35,5 +35,5 @@ void mainCommon(AppFlavor flavor) { // ErrorWidget.builder = (details) => ErrorWidget(details.exception); // cf https://docs.flutter.dev/testing/errors - runApp(AvesApp(flavor: flavor)); + runApp(AvesApp(flavor: flavor, debugIntentData: debugIntentData)); } diff --git a/lib/main_play_test_editor.dart b/lib/main_play_test_editor.dart new file mode 100644 index 000000000..95e5f4d6c --- /dev/null +++ b/lib/main_play_test_editor.dart @@ -0,0 +1,17 @@ +import 'package:aves/app_flavor.dart'; +import 'package:aves/main_common.dart'; +import 'package:aves/widgets/intent.dart'; + +// https://developer.android.com/studio/command-line/adb.html#IntentSpec +// adb shell am start -n deckers.thibault.aves.debug/deckers.thibault.aves.MainActivity -a android.intent.action.EDIT -d content://media/external/images/media/183128 -t image/* + +@pragma('vm:entry-point') +void main() => mainCommon( + AppFlavor.play, + debugIntentData: { + IntentDataKeys.action: IntentActions.edit, + IntentDataKeys.mimeType: 'image/*', + IntentDataKeys.uri: 'content://media/external/images/media/183128', + // IntentDataKeys.uri: 'content://media/external/images/media/183534', + }, + ); diff --git a/lib/model/app/contributors.dart b/lib/model/app/contributors.dart index 26b45a414..e97c25d1c 100644 --- a/lib/model/app/contributors.dart +++ b/lib/model/app/contributors.dart @@ -44,17 +44,19 @@ class Contributors { Contributor('Dick Pluim', 'github@dickpluim.com'), Contributor('György Viktor', 'wickdj@gmail.com'), Contributor('byPety', 'peter@csordascsalad.hu'), + Contributor('tryvseu', 'tryvseu@tuta.io'), // Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'), // Arabic // Contributor('Salih Ail', 'rrrfff444@gmail.com'), // Arabic + // Contributor('nasreddineloukriz', 'nasreddineloukriz@gmail.com'), // Arabic // Contributor('امیر جهانگرد', 'ijahangard.a@gmail.com'), // Persian // Contributor('slasb37', 'p84haghi@gmail.com'), // Persian - // Contributor('tryvseu', 'tryvseu@tuta.io'), // Nynorsk // Contributor('Nattapong K', 'mixer5056@gmail.com'), // Thai // Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew // Contributor('Martin Frandel', 'martinko.fr@gmail.com'), // Slovak // Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central) // Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi // Contributor('Subham Jena', 'subhamjena8465@gmail.com'), // Odia + // Contributor('Raman', 'xysed@tutanota.com'), // Malayalam }; } diff --git a/lib/model/app/dependencies.dart b/lib/model/app/dependencies.dart index dedcba43d..794029ea8 100644 --- a/lib/model/app/dependencies.dart +++ b/lib/model/app/dependencies.dart @@ -2,10 +2,11 @@ import 'package:aves/app_flavor.dart'; class Dependencies { static const String apache2 = 'Apache License 2.0'; - static const String bsd2 = 'BSD 2-Clause "Simplified" License'; - static const String bsd3 = 'BSD 3-Clause "Revised" License'; + static const String bsd2 = 'BSD 2-Clause “Simplified” License'; + static const String bsd3 = 'BSD 3-Clause “Revised” License'; static const String eclipse1 = 'Eclipse Public License 1.0'; static const String mit = 'MIT License'; + static const String zlib = 'zlib License'; static const List androidDependencies = [ Dependency( @@ -88,8 +89,8 @@ class Dependencies { Dependency( name: 'Local Auth', license: bsd3, - licenseUrl: 'https://github.com/flutter/plugins/blob/main/packages/local_auth/local_auth/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth', + licenseUrl: 'https://github.com/flutter/packages/blob/main/packages/local_auth/local_auth/LICENSE', + sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth', ), Dependency( name: 'Package Info Plus', @@ -115,8 +116,8 @@ class Dependencies { Dependency( name: 'Shared Preferences', license: bsd3, - licenseUrl: 'https://github.com/flutter/plugins/blob/main/packages/shared_preferences/shared_preferences/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences', + licenseUrl: 'https://github.com/flutter/packages/blob/main/packages/shared_preferences/shared_preferences/LICENSE', + sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences', ), Dependency( name: 'sqflite', @@ -131,8 +132,8 @@ class Dependencies { Dependency( name: 'URL Launcher', license: bsd3, - licenseUrl: 'https://github.com/flutter/plugins/blob/main/packages/url_launcher/url_launcher/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher', + licenseUrl: 'https://github.com/flutter/packages/blob/main/packages/url_launcher/url_launcher/LICENSE', + sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher', ), Dependency( name: 'Volume Controller', @@ -150,8 +151,8 @@ class Dependencies { Dependency( name: 'Google Maps for Flutter', license: bsd3, - licenseUrl: 'https://github.com/flutter/plugins/blob/main/packages/google_maps_flutter/google_maps_flutter/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter', + licenseUrl: 'https://github.com/flutter/packages/blob/main/packages/google_maps_flutter/google_maps_flutter/LICENSE', + sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter', ), ]; @@ -369,6 +370,11 @@ class Dependencies { license: bsd2, sourceUrl: 'https://github.com/google/tuple.dart', ), + Dependency( + name: 'Vector Math', + license: '$zlib, $bsd3', + sourceUrl: 'https://github.com/google/vector_math.dart', + ), Dependency( name: 'XML', license: mit, diff --git a/lib/model/db/db_metadata_sqflite_upgrade.dart b/lib/model/db/db_metadata_sqflite_upgrade.dart index f2840e2ee..ef0b66074 100644 --- a/lib/model/db/db_metadata_sqflite_upgrade.dart +++ b/lib/model/db/db_metadata_sqflite_upgrade.dart @@ -21,34 +21,24 @@ class MetadataDbUpgrader { switch (oldVersion) { case 1: await _upgradeFrom1(db); - break; case 2: await _upgradeFrom2(db); - break; case 3: await _upgradeFrom3(db); - break; case 4: await _upgradeFrom4(db); - break; case 5: await _upgradeFrom5(db); - break; case 6: await _upgradeFrom6(db); - break; case 7: await _upgradeFrom7(db); - break; case 8: await _upgradeFrom8(db); - break; case 9: await _upgradeFrom9(db); - break; case 10: await _upgradeFrom10(db); - break; } oldVersion++; } diff --git a/lib/model/device.dart b/lib/model/device.dart index 2b431ac87..c70c1d8ed 100644 --- a/lib/model/device.dart +++ b/lib/model/device.dart @@ -9,7 +9,7 @@ final Device device = Device._private(); class Device { late final String _userAgent; - late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint; + late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut; late final bool _canRenderFlagEmojis, _canRenderSubdivisionFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto; late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode, _supportPictureInPicture; @@ -21,8 +21,6 @@ class Device { bool get canPinShortcut => _canPinShortcut; - bool get canPrint => _canPrint; - bool get canRenderFlagEmojis => _canRenderFlagEmojis; bool get canRenderSubdivisionFlagEmojis => _canRenderSubdivisionFlagEmojis; @@ -71,7 +69,6 @@ class Device { final capabilities = await deviceService.getCapabilities(); _canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false; _canPinShortcut = capabilities['canPinShortcut'] ?? false; - _canPrint = capabilities['canPrint'] ?? false; _canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false; _canRenderSubdivisionFlagEmojis = capabilities['canRenderSubdivisionFlagEmojis'] ?? false; _canRequestManageMedia = capabilities['canRequestManageMedia'] ?? false; diff --git a/lib/model/entry/extensions/catalog.dart b/lib/model/entry/extensions/catalog.dart index 6a7aa331e..73ee03ac6 100644 --- a/lib/model/entry/extensions/catalog.dart +++ b/lib/model/entry/extensions/catalog.dart @@ -39,7 +39,7 @@ extension ExtraAvesEntryCatalog on AvesEntry { if (isGeotiff && !hasGps) { final info = await metadataFetchService.getGeoTiffInfo(this); if (info != null) { - final center = MappedGeoTiff( + final center = GeoTiffCoordinateConverter( info: info, entry: this, ).center; diff --git a/lib/model/entry/extensions/metadata_edition.dart b/lib/model/entry/extensions/metadata_edition.dart index 9d1f2f7c3..c28c26d8d 100644 --- a/lib/model/entry/extensions/metadata_edition.dart +++ b/lib/model/entry/extensions/metadata_edition.dart @@ -52,7 +52,6 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { case DateEditAction.copyItem: case DateEditAction.extractFromTitle: editCreateDateXmp(descriptions, appliedModifier.setDateTime); - break; case DateEditAction.shift: final xmpDate = XMP.getString(descriptions, XmpAttributes.xmpCreateDate, namespace: XmpNamespaces.xmp); if (xmpDate != null) { @@ -65,10 +64,8 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { reportService.recordError('failed to parse XMP date=$xmpDate', null); } } - break; case DateEditAction.remove: editCreateDateXmp(descriptions, null); - break; } return true; }), @@ -541,10 +538,8 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { } } } on FileSystemException catch (_) {} - break; default: date = await metadataFetchService.getDate(this, source.toMetadataField()!); - break; } } return date != null ? DateModifier.setCustom(mainMetadataDate(), date) : null; diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index 7f5f8aa77..34e4ab626 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -78,7 +78,6 @@ class AlbumFilter extends CoveredCollectionFilter { case AlbumType.app: final appColor = colors.appColor(album); if (appColor != null) return appColor; - break; case AlbumType.camera: return SynchronousFuture(colors.albumCamera); case AlbumType.download: diff --git a/lib/model/filters/aspect_ratio.dart b/lib/model/filters/aspect_ratio.dart index 92ee97279..f21f48280 100644 --- a/lib/model/filters/aspect_ratio.dart +++ b/lib/model/filters/aspect_ratio.dart @@ -21,13 +21,10 @@ class AspectRatioFilter extends CollectionFilter { switch (op) { case QueryFilter.opEqual: _test = (entry) => entry.displayAspectRatio == threshold; - break; case QueryFilter.opLower: _test = (entry) => entry.displayAspectRatio < threshold; - break; case QueryFilter.opGreater: _test = (entry) => entry.displayAspectRatio > threshold; - break; } } diff --git a/lib/model/filters/date.dart b/lib/model/filters/date.dart index 3c82ae15c..4749fcd4d 100644 --- a/lib/model/filters/date.dart +++ b/lib/model/filters/date.dart @@ -25,13 +25,10 @@ class DateFilter extends CollectionFilter { switch (level) { case DateLevel.y: _test = (entry) => entry.bestDate?.isAtSameYearAs(_effectiveDate) ?? false; - break; case DateLevel.ym: _test = (entry) => entry.bestDate?.isAtSameMonthAs(_effectiveDate) ?? false; - break; case DateLevel.ymd: _test = (entry) => entry.bestDate?.isAtSameDayAs(_effectiveDate) ?? false; - break; case DateLevel.md: final month = _effectiveDate.month; final day = _effectiveDate.day; @@ -39,15 +36,12 @@ class DateFilter extends CollectionFilter { final bestDate = entry.bestDate; return bestDate != null && bestDate.month == month && bestDate.day == day; }; - break; case DateLevel.m: final month = _effectiveDate.month; _test = (entry) => entry.bestDate?.month == month; - break; case DateLevel.d: final day = _effectiveDate.day; _test = (entry) => entry.bestDate?.day == day; - break; } } diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index 0c547defd..3a22391b5 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -29,13 +29,10 @@ class LocationFilter extends CoveredCollectionFilter { switch (level) { case LocationLevel.country: _test = (entry) => entry.addressDetails?.countryCode == _code; - break; case LocationLevel.state: _test = (entry) => entry.addressDetails?.stateCode == _code; - break; case LocationLevel.place: _test = (entry) => entry.addressDetails?.place == _location; - break; } } } @@ -57,7 +54,6 @@ class LocationFilter extends CoveredCollectionFilter { if (_code != null) { location = _nameAndCode; } - break; case LocationLevel.place: break; } diff --git a/lib/model/filters/missing.dart b/lib/model/filters/missing.dart index 6060d3f36..9128d1eb1 100644 --- a/lib/model/filters/missing.dart +++ b/lib/model/filters/missing.dart @@ -26,15 +26,12 @@ class MissingFilter extends CollectionFilter { case _date: _test = (entry) => (entry.catalogMetadata?.dateMillis ?? 0) == 0; _icon = AIcons.dateUndated; - break; case _fineAddress: _test = (entry) => entry.hasGps && !entry.hasFineAddress; _icon = AIcons.locationUnlocated; - break; case _title: _test = (entry) => (entry.catalogMetadata?.xmpTitle ?? '').isEmpty; _icon = AIcons.descriptionUntitled; - break; } } diff --git a/lib/model/filters/placeholder.dart b/lib/model/filters/placeholder.dart index 32f22b1f4..bbeab3bed 100644 --- a/lib/model/filters/placeholder.dart +++ b/lib/model/filters/placeholder.dart @@ -28,13 +28,10 @@ class PlaceholderFilter extends CollectionFilter { switch (placeholder) { case _country: _icon = AIcons.country; - break; case _state: _icon = AIcons.state; - break; case _place: _icon = AIcons.place; - break; } } @@ -74,7 +71,6 @@ class PlaceholderFilter extends CollectionFilter { case _place: return address.place; } - break; } return null; } diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index 523a1b156..2c1689551 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -117,7 +117,6 @@ class QueryFilter extends CollectionFilter { if (op == opEqual) { return (entry) => entry.contentId == valueInt; } - break; case keyContentYear: if (valueInt == null) return null; switch (op) { @@ -128,7 +127,6 @@ class QueryFilter extends CollectionFilter { case opGreater: return (entry) => (entry.bestDate?.year ?? 0) > valueInt; } - break; case keyContentMonth: if (valueInt == null) return null; switch (op) { @@ -139,7 +137,6 @@ class QueryFilter extends CollectionFilter { case opGreater: return (entry) => (entry.bestDate?.month ?? 0) > valueInt; } - break; case keyContentDay: if (valueInt == null) return null; switch (op) { @@ -150,7 +147,6 @@ class QueryFilter extends CollectionFilter { case opGreater: return (entry) => (entry.bestDate?.day ?? 0) > valueInt; } - break; case keyContentWidth: if (valueInt == null) return null; switch (op) { @@ -161,7 +157,6 @@ class QueryFilter extends CollectionFilter { case opGreater: return (entry) => entry.displaySize.width > valueInt; } - break; case keyContentHeight: if (valueInt == null) return null; switch (op) { @@ -172,7 +167,6 @@ class QueryFilter extends CollectionFilter { case opGreater: return (entry) => entry.displaySize.height > valueInt; } - break; case keyContentSize: match = _fileSizePattern.firstMatch(valueString); if (match == null) return null; @@ -187,13 +181,10 @@ class QueryFilter extends CollectionFilter { switch (multiplierString) { case 'K': bytes *= kilo; - break; case 'M': bytes *= mega; - break; case 'G': bytes *= giga; - break; } switch (op) { @@ -204,7 +195,6 @@ class QueryFilter extends CollectionFilter { case opGreater: return (entry) => (entry.sizeBytes ?? 0) > bytes; } - break; } return null; diff --git a/lib/model/filters/type.dart b/lib/model/filters/type.dart index 9eafa93b3..b94dcb6ac 100644 --- a/lib/model/filters/type.dart +++ b/lib/model/filters/type.dart @@ -37,27 +37,21 @@ class TypeFilter extends CollectionFilter { case _animated: _test = (entry) => entry.isAnimated; _icon = AIcons.animated; - break; case _geotiff: _test = (entry) => entry.isGeotiff; _icon = AIcons.geo; - break; case _motionPhoto: _test = (entry) => entry.isMotionPhoto; _icon = AIcons.motionPhoto; - break; case _panorama: _test = (entry) => entry.isImage && entry.is360; _icon = AIcons.panorama; - break; case _raw: _test = (entry) => entry.isRaw; _icon = AIcons.raw; - break; case _sphericalVideo: _test = (entry) => entry.isVideo && entry.is360; _icon = AIcons.sphericalVideo; - break; } } diff --git a/lib/model/geotiff.dart b/lib/model/geotiff.dart index efa6afb46..d549cd29d 100644 --- a/lib/model/geotiff.dart +++ b/lib/model/geotiff.dart @@ -8,8 +8,7 @@ import 'package:aves/ref/metadata/geotiff.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:aves_map/aves_map.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/painting.dart'; +import 'package:flutter/widgets.dart'; import 'package:latlong2/latlong.dart'; import 'package:proj4dart/proj4dart.dart' as proj4; @@ -42,19 +41,145 @@ class GeoTiffInfo extends Equatable { class MappedGeoTiff with MapOverlay { final AvesEntry entry; - late LatLng? Function(Point pixel) pointToLatLng; - late Point? Function(Point smPoint) epsg3857ToPoint; - static final mapServiceTileSize = (256 * ui.window.devicePixelRatio).round(); - static final mapServiceHelper = MapServiceHelper(mapServiceTileSize); - static final tileImagePaint = Paint(); - static final tileMissingPaint = Paint() + late final GeoTiffCoordinateConverter _converter; + late final int _mapServiceTileSize; + late final MapServiceHelper _mapServiceHelper; + + static final _tileImagePaint = Paint(); + static final _tileMissingPaint = Paint() ..style = PaintingStyle.fill ..color = const Color(0xFF000000); MappedGeoTiff({ required GeoTiffInfo info, required this.entry, + required double devicePixelRatio, + }) { + _converter = GeoTiffCoordinateConverter(info: info, entry: entry); + _mapServiceTileSize = (256 * devicePixelRatio).round(); + _mapServiceHelper = MapServiceHelper(_mapServiceTileSize); + } + + @override + Future getTile(int tx, int ty, int? zoomLevel) async { + zoomLevel ??= 0; + + // global projected coordinates in meters (EPSG:3857 Spherical Mercator) + final tileTopLeft3857 = _mapServiceHelper.tileTopLeft(tx, ty, zoomLevel); + final tileBottomRight3857 = _mapServiceHelper.tileTopLeft(tx + 1, ty + 1, zoomLevel); + + // image region coordinates in pixels + final tileTopLeftPx = _converter.epsg3857ToPoint(tileTopLeft3857); + final tileBottomRightPx = _converter.epsg3857ToPoint(tileBottomRight3857); + if (tileTopLeftPx == null || tileBottomRightPx == null) return null; + + final tileLeft = tileTopLeftPx.x; + final tileRight = tileBottomRightPx.x; + final tileTop = tileTopLeftPx.y; + final tileBottom = tileBottomRightPx.y; + + final width = entry.width; + final height = entry.height; + + final regionLeft = tileLeft.clamp(0, width); + final regionRight = tileRight.clamp(0, width); + final regionTop = tileTop.clamp(0, height); + final regionBottom = tileBottom.clamp(0, height); + + final regionWidth = regionRight - regionLeft; + final regionHeight = regionBottom - regionTop; + if (regionWidth == 0 || regionHeight == 0) return null; + + final tileXScale = (tileRight - tileLeft) / _mapServiceTileSize; + final sampleSize = max(1, highestPowerOf2(tileXScale)); + final region = entry.getRegion( + sampleSize: sampleSize, + region: Rectangle(regionLeft, regionTop, regionWidth, regionHeight), + ); + + final imageInfoCompleter = Completer(); + final imageStream = region.resolve(ImageConfiguration.empty); + final imageStreamListener = ImageStreamListener((image, synchronousCall) { + imageInfoCompleter.complete(image); + }, onError: imageInfoCompleter.completeError); + imageStream.addListener(imageStreamListener); + ImageInfo? regionImageInfo; + try { + regionImageInfo = await imageInfoCompleter.future; + } catch (error) { + debugPrint('failed to get image for region=$region with error=$error'); + } + imageStream.removeListener(imageStreamListener); + + final imageOffset = Offset( + regionLeft > tileLeft ? (regionLeft - tileLeft).toDouble() : 0, + regionTop > tileTop ? (regionTop - tileTop).toDouble() : 0, + ); + final tileImageScaleX = (tileRight - tileLeft) / _mapServiceTileSize; + final tileImageScaleY = (tileBottom - tileTop) / _mapServiceTileSize; + + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + canvas.scale(1 / tileImageScaleX, 1 / tileImageScaleY); + if (regionImageInfo != null) { + final s = sampleSize.toDouble(); + canvas.scale(s, s); + canvas.drawImage(regionImageInfo.image, imageOffset / s, _tileImagePaint); + canvas.scale(1 / s, 1 / s); + } else { + // fallback to show area + canvas.drawRect( + Rect.fromLTWH( + imageOffset.dx, + imageOffset.dy, + regionWidth.toDouble(), + regionHeight.toDouble(), + ), + _tileMissingPaint, + ); + } + canvas.scale(tileImageScaleX, tileImageScaleY); + + final picture = recorder.endRecording(); + final tileImage = await picture.toImage(_mapServiceTileSize, _mapServiceTileSize); + final byteData = await tileImage.toByteData(format: ui.ImageByteFormat.png); + if (byteData == null) return null; + + return MapTile( + width: tileImage.width, + height: tileImage.height, + data: byteData.buffer.asUint8List(), + ); + } + + @override + String get id => entry.uri; + + @override + ImageProvider get imageProvider => entry.uriImage; + + @override + bool get canOverlay => center != null; + + LatLng? get center => _converter.center; + + @override + LatLng? get topLeft => _converter.topLeft; + + @override + LatLng? get bottomRight => _converter.bottomRight; +} + +class GeoTiffCoordinateConverter { + final AvesEntry entry; + + late LatLng? Function(Point pixel) pointToLatLng; + late Point? Function(Point smPoint) epsg3857ToPoint; + + GeoTiffCoordinateConverter({ + required GeoTiffInfo info, + required this.entry, }) { pointToLatLng = (_) => null; epsg3857ToPoint = (_) => null; @@ -129,113 +254,13 @@ class MappedGeoTiff with MapOverlay { }; } - @override - Future getTile(int tx, int ty, int? zoomLevel) async { - zoomLevel ??= 0; - - // global projected coordinates in meters (EPSG:3857 Spherical Mercator) - final tileTopLeft3857 = mapServiceHelper.tileTopLeft(tx, ty, zoomLevel); - final tileBottomRight3857 = mapServiceHelper.tileTopLeft(tx + 1, ty + 1, zoomLevel); - - // image region coordinates in pixels - final tileTopLeftPx = epsg3857ToPoint(tileTopLeft3857); - final tileBottomRightPx = epsg3857ToPoint(tileBottomRight3857); - if (tileTopLeftPx == null || tileBottomRightPx == null) return null; - - final tileLeft = tileTopLeftPx.x; - final tileRight = tileBottomRightPx.x; - final tileTop = tileTopLeftPx.y; - final tileBottom = tileBottomRightPx.y; - - final regionLeft = tileLeft.clamp(0, width); - final regionRight = tileRight.clamp(0, width); - final regionTop = tileTop.clamp(0, height); - final regionBottom = tileBottom.clamp(0, height); - - final regionWidth = regionRight - regionLeft; - final regionHeight = regionBottom - regionTop; - if (regionWidth == 0 || regionHeight == 0) return null; - - final tileXScale = (tileRight - tileLeft) / mapServiceTileSize; - final sampleSize = max(1, highestPowerOf2(tileXScale)); - final region = entry.getRegion( - sampleSize: sampleSize, - region: Rectangle(regionLeft, regionTop, regionWidth, regionHeight), - ); - - final imageInfoCompleter = Completer(); - final imageStream = region.resolve(ImageConfiguration.empty); - final imageStreamListener = ImageStreamListener((image, synchronousCall) { - imageInfoCompleter.complete(image); - }, onError: imageInfoCompleter.completeError); - imageStream.addListener(imageStreamListener); - ImageInfo? regionImageInfo; - try { - regionImageInfo = await imageInfoCompleter.future; - } catch (error) { - debugPrint('failed to get image for region=$region with error=$error'); - } - imageStream.removeListener(imageStreamListener); - - final imageOffset = Offset( - regionLeft > tileLeft ? (regionLeft - tileLeft).toDouble() : 0, - regionTop > tileTop ? (regionTop - tileTop).toDouble() : 0, - ); - final tileImageScaleX = (tileRight - tileLeft) / mapServiceTileSize; - final tileImageScaleY = (tileBottom - tileTop) / mapServiceTileSize; - - final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - canvas.scale(1 / tileImageScaleX, 1 / tileImageScaleY); - if (regionImageInfo != null) { - final s = sampleSize.toDouble(); - canvas.scale(s, s); - canvas.drawImage(regionImageInfo.image, imageOffset / s, tileImagePaint); - canvas.scale(1 / s, 1 / s); - } else { - // fallback to show area - canvas.drawRect( - Rect.fromLTWH( - imageOffset.dx, - imageOffset.dy, - regionWidth.toDouble(), - regionHeight.toDouble(), - ), - tileMissingPaint, - ); - } - canvas.scale(tileImageScaleX, tileImageScaleY); - - final picture = recorder.endRecording(); - final tileImage = await picture.toImage(mapServiceTileSize, mapServiceTileSize); - final byteData = await tileImage.toByteData(format: ui.ImageByteFormat.png); - if (byteData == null) return null; - - return MapTile( - width: tileImage.width, - height: tileImage.height, - data: byteData.buffer.asUint8List(), - ); - } - - @override - String get id => entry.uri; - - @override - ImageProvider get imageProvider => entry.uriImage; - int get width => entry.width; int get height => entry.height; - @override - bool get canOverlay => center != null; - LatLng? get center => pointToLatLng(Point((width / 2).round(), (height / 2).round())); - @override LatLng? get topLeft => pointToLatLng(const Point(0, 0)); - @override LatLng? get bottomRight => pointToLatLng(Point(width, height)); } diff --git a/lib/model/naming_pattern.dart b/lib/model/naming_pattern.dart index 2fd502291..e5eee02d4 100644 --- a/lib/model/naming_pattern.dart +++ b/lib/model/naming_pattern.dart @@ -38,10 +38,8 @@ class NamingPattern { if (processorOptions != null) { processors.add(DateNamingProcessor(processorOptions.trim())); } - break; case NameNamingProcessor.key: processors.add(const NameNamingProcessor()); - break; case CounterNamingProcessor.key: int? start, padding; _applyProcessorOptions(processorOptions, (key, value) { @@ -50,18 +48,14 @@ class NamingPattern { switch (key) { case CounterNamingProcessor.optionStart: start = valueInt; - break; case CounterNamingProcessor.optionPadding: padding = valueInt; - break; } } }); processors.add(CounterNamingProcessor(start: start ?? defaultCounterStart, padding: padding ?? defaultCounterPadding)); - break; default: debugPrint('unsupported naming processor: ${match.group(0)}'); - break; } index = end; }); diff --git a/lib/model/query.dart b/lib/model/query.dart index 82be471d9..29d950240 100644 --- a/lib/model/query.dart +++ b/lib/model/query.dart @@ -8,7 +8,8 @@ class Query extends ChangeNotifier { final ValueNotifier _queryNotifier = ValueNotifier(''); final StreamController _enabledStreamController = StreamController.broadcast(); - Query({required String? initialValue}) { + Query({required bool enabled, required String? initialValue}) { + _enabled = enabled; if (initialValue != null && initialValue.isNotEmpty) { _enabled = true; queryNotifier.value = initialValue; diff --git a/lib/model/settings/enums/display_refresh_rate_mode.dart b/lib/model/settings/enums/display_refresh_rate_mode.dart index 942d901b9..32d1cd28b 100644 --- a/lib/model/settings/enums/display_refresh_rate_mode.dart +++ b/lib/model/settings/enums/display_refresh_rate_mode.dart @@ -15,13 +15,10 @@ extension ExtraDisplayRefreshRateMode on DisplayRefreshRateMode { switch (this) { case DisplayRefreshRateMode.auto: await FlutterDisplayMode.setPreferredMode(DisplayMode.auto); - break; case DisplayRefreshRateMode.highest: await FlutterDisplayMode.setHighRefreshRate(); - break; case DisplayRefreshRateMode.lowest: await FlutterDisplayMode.setLowRefreshRate(); - break; } } } diff --git a/lib/model/settings/enums/home_page.dart b/lib/model/settings/enums/home_page.dart index 5adb6788b..cf33a0fc4 100644 --- a/lib/model/settings/enums/home_page.dart +++ b/lib/model/settings/enums/home_page.dart @@ -1,5 +1,6 @@ import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; +import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:aves_model/aves_model.dart'; extension ExtraHomePageSetting on HomePageSetting { @@ -9,6 +10,8 @@ extension ExtraHomePageSetting on HomePageSetting { return CollectionPage.routeName; case HomePageSetting.albums: return AlbumListPage.routeName; + case HomePageSetting.tags: + return TagListPage.routeName; } } } diff --git a/lib/model/settings/enums/viewer_transition.dart b/lib/model/settings/enums/viewer_transition.dart index b97b5b77c..81aefe62a 100644 --- a/lib/model/settings/enums/viewer_transition.dart +++ b/lib/model/settings/enums/viewer_transition.dart @@ -1,5 +1,8 @@ +import 'dart:math'; + import 'package:aves/widgets/viewer/controls/controller.dart'; import 'package:aves_model/aves_model.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; extension ExtraViewerTransition on ViewerTransition { @@ -15,6 +18,46 @@ extension ExtraViewerTransition on ViewerTransition { return PageTransitionEffects.fade(pageController, index, zoomIn: true); case ViewerTransition.none: return PageTransitionEffects.none(pageController, index); + case ViewerTransition.random: + return _ViewerTransitionRandomizer.getBuilder(pageController, index); } } } + +class _ViewerTransitionRandomizer { + static const options = [ + ViewerTransition.slide, + ViewerTransition.parallax, + ViewerTransition.fade, + ViewerTransition.zoomIn, + ]; + + static final List<(int, ViewerTransition)> _indexedTransitions = []; + + static TransitionBuilder getBuilder( + PageController pageController, + int index, + ) => + (context, child) { + final negative = pageController.hasClients && pageController.position.haveDimensions && (pageController.page! - index).isNegative; + final transition = _getTransition(negative ? index - 1 : index); + final builder = transition.builder(pageController, index); + return builder(context, child); + }; + + static ViewerTransition _getTransition(int transitionIndex) { + var indexedTransition = _indexedTransitions.firstWhereOrNull((v) => v.$1 == transitionIndex); + if (indexedTransition != null) { + _indexedTransitions.remove(indexedTransition); + } else { + indexedTransition = (transitionIndex, options[Random().nextInt(options.length)]); + } + _indexedTransitions.insert(0, indexedTransition); + while (_indexedTransitions.length > 3) { + _indexedTransitions.removeLast(); + } + + final (_, transition) = indexedTransition; + return transition; + } +} diff --git a/lib/model/settings/enums/widget_shape.dart b/lib/model/settings/enums/widget_shape.dart index adb2d6730..28a64f3ab 100644 --- a/lib/model/settings/enums/widget_shape.dart +++ b/lib/model/settings/enums/widget_shape.dart @@ -4,7 +4,7 @@ import 'package:flutter/painting.dart'; extension ExtraWidgetShape on WidgetShape { Path path(Size widgetSize, double devicePixelRatio) { - final rect = Rect.fromLTWH(0, 0, widgetSize.width, widgetSize.height); + final rect = Offset.zero & widgetSize; switch (this) { case WidgetShape.rrect: return Path()..addRRect(BorderRadius.circular(24 * devicePixelRatio).toRRect(rect)); diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 86a9019ea..966b545db 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -118,6 +118,7 @@ class Settings extends ChangeNotifier { static const tagSortReverseKey = 'tag_sort_reverse'; static const pinnedFiltersKey = 'pinned_filters'; static const hiddenFiltersKey = 'hidden_filters'; + static const showAlbumPickQueryKey = 'show_album_pick_query'; // viewer static const viewerQuickActionsKey = 'viewer_quick_actions'; @@ -374,7 +375,7 @@ class Settings extends ChangeNotifier { if (_locale != null) { preferredLocales.add(_locale); } else { - preferredLocales.addAll(WidgetsBinding.instance.window.locales); + preferredLocales.addAll(WidgetsBinding.instance.platformDispatcher.locales); if (preferredLocales.isEmpty) { // the `window` locales may be empty in a window-less service context preferredLocales.addAll(_systemLocalesFallback); @@ -625,6 +626,10 @@ class Settings extends ChangeNotifier { hiddenFilters = _hiddenFilters; } + bool get showAlbumPickQuery => getBool(showAlbumPickQueryKey) ?? false; + + set showAlbumPickQuery(bool newValue) => _set(showAlbumPickQueryKey, newValue); + // viewer List get viewerQuickActions => getEnumListOrDefault(viewerQuickActionsKey, SettingsDefaults.viewerQuickActions, EntryAction.values); @@ -1017,7 +1022,6 @@ class Settings extends ChangeNotifier { if (value is num) { isRotationLocked = value == 0; } - break; case platformTransitionAnimationScaleKey: if (value is num) { areAnimationsRemoved = value == 0; @@ -1075,7 +1079,6 @@ class Settings extends ChangeNotifier { } else { debugPrint('failed to import key=$key, value=$newValue is not an int'); } - break; case subtitleFontSizeKey: case infoMapZoomKey: if (newValue is double) { @@ -1083,7 +1086,6 @@ class Settings extends ChangeNotifier { } else { debugPrint('failed to import key=$key, value=$newValue is not a double'); } - break; case isInstalledAppAccessAllowedKey: case isErrorReportingAllowedKey: case enableDynamicColorKey: @@ -1107,6 +1109,7 @@ class Settings extends ChangeNotifier { case stateSortReverseKey: case placeSortReverseKey: case tagSortReverseKey: + case showAlbumPickQueryKey: case showOverlayOnOpeningKey: case showOverlayMinimapKey: case showOverlayInfoKey: @@ -1138,7 +1141,6 @@ class Settings extends ChangeNotifier { } else { debugPrint('failed to import key=$key, value=$newValue is not a bool'); } - break; case localeKey: case displayRefreshRateModeKey: case themeBrightnessKey: @@ -1181,7 +1183,6 @@ class Settings extends ChangeNotifier { } else { debugPrint('failed to import key=$key, value=$newValue is not a string'); } - break; case drawerTypeBookmarksKey: case drawerAlbumBookmarksKey: case drawerPageBookmarksKey: @@ -1197,7 +1198,6 @@ class Settings extends ChangeNotifier { } else { debugPrint('failed to import key=$key, value=$newValue is not a list'); } - break; } } if (oldValue != newValue) { diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index e4524418e..1a2c09bd4 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.dart @@ -47,13 +47,10 @@ mixin AlbumMixin on SourceBase { switch (androidFileUtils.getAlbumType(album)) { case AlbumType.regular: regularAlbums.add(album); - break; case AlbumType.app: appAlbums.add(album); - break; default: specialAlbums.add(album); - break; } } return Map.fromEntries([...specialAlbums, ...appAlbums, ...regularAlbums].map((album) => MapEntry( @@ -93,7 +90,11 @@ mixin AlbumMixin on SourceBase { } bool _isRemovable(String album) { - return !(visibleEntries.any((entry) => entry.directory == album) || _newAlbums.contains(album) || vaults.isVault(album)); + if (visibleEntries.any((entry) => entry.directory == album)) return false; + if (_newAlbums.contains(album)) return false; + if (vaults.isVault(album)) return false; + if (settings.pinnedFilters.whereType().map((v) => v.album).contains(album)) return false; + return true; } // filter summary diff --git a/lib/model/source/analysis_controller.dart b/lib/model/source/analysis_controller.dart index c4e1e9e41..f6996674c 100644 --- a/lib/model/source/analysis_controller.dart +++ b/lib/model/source/analysis_controller.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; class AnalysisController { final bool canStartService, force; + final int progressTotal, progressOffset; final List? entryIds; final ValueNotifier stopSignal; @@ -9,6 +10,8 @@ class AnalysisController { this.canStartService = true, this.entryIds, this.force = false, + this.progressTotal = 0, + this.progressOffset = 0, ValueNotifier? stopSignal, }) : stopSignal = stopSignal ?? ValueNotifier(false); diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index 44b37f548..dfc922a3b 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -68,10 +68,8 @@ class CollectionLens with ChangeNotifier { case MoveType.move: case MoveType.fromBin: refresh(); - break; case MoveType.toBin: _onEntryRemoved(e.entries); - break; } })); _subscriptions.add(sourceEvents.on().listen((e) => refresh())); @@ -213,16 +211,12 @@ class CollectionLens with ChangeNotifier { switch (sortFactor) { case EntrySortFactor.date: _filteredSortedEntries.sort(AvesEntrySort.compareByDate); - break; case EntrySortFactor.name: _filteredSortedEntries.sort(AvesEntrySort.compareByName); - break; case EntrySortFactor.rating: _filteredSortedEntries.sort(AvesEntrySort.compareByRating); - break; case EntrySortFactor.size: _filteredSortedEntries.sort(AvesEntrySort.compareBySize); - break; } if (sortReverse) { _filteredSortedEntries = _filteredSortedEntries.reversed.toList(); @@ -240,33 +234,25 @@ class CollectionLens with ChangeNotifier { switch (sectionFactor) { case EntryGroupFactor.album: sections = groupBy(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory)); - break; case EntryGroupFactor.month: sections = groupBy(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.monthTaken)); - break; case EntryGroupFactor.day: sections = groupBy(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.dayTaken)); - break; case EntryGroupFactor.none: sections = Map.fromEntries([ MapEntry(const SectionKey(), _filteredSortedEntries), ]); - break; } - break; case EntrySortFactor.name: final byAlbum = groupBy(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory)); final compare = sortReverse ? (a, b) => source.compareAlbumsByName(b.directory!, a.directory!) : (a, b) => source.compareAlbumsByName(a.directory!, b.directory!); sections = SplayTreeMap>.of(byAlbum, compare); - break; case EntrySortFactor.rating: sections = groupBy(_filteredSortedEntries, (entry) => EntryRatingSectionKey(entry.rating)); - break; case EntrySortFactor.size: sections = Map.fromEntries([ MapEntry(const SectionKey(), _filteredSortedEntries), ]); - break; } } sections = Map.unmodifiable(sections); diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index ec7d49304..e12ee7da3 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -221,18 +221,14 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place switch (key) { case 'contentId': entry.contentId = newValue as int?; - break; case 'dateModifiedSecs': // `dateModifiedSecs` changes when moving entries to another directory, // but it does not change when renaming the containing directory entry.dateModifiedSecs = newValue as int?; - break; case 'path': entry.path = newValue as String?; - break; case 'title': entry.sourceTitle = newValue as String?; - break; case 'trashed': final trashed = newValue as bool; entry.trashed = trashed; @@ -243,13 +239,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place dateMillis: DateTime.now().millisecondsSinceEpoch, ) : null; - break; case 'uri': entry.uri = newValue as String; - break; case 'origin': entry.origin = newValue as int; - break; } }); if (entry.trashed) { @@ -299,7 +292,9 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place settings.drawerAlbumBookmarks = settings.drawerAlbumBookmarks?..insert(bookmark, destinationAlbum); } if (pinned) { - settings.pinnedFilters = settings.pinnedFilters..add(newFilter); + settings.pinnedFilters = settings.pinnedFilters + ..remove(oldFilter) + ..add(newFilter); } } @@ -371,16 +366,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place switch (moveType) { case MoveType.copy: addEntries(movedEntries); - break; case MoveType.move: case MoveType.export: cleanEmptyAlbums(fromAlbums.whereNotNull().toSet()); addDirectories(albums: destinationAlbums); - break; case MoveType.toBin: case MoveType.fromBin: updateDerivedFilters(movedEntries); - break; } invalidateAlbumFilterSummary(directories: fromAlbums); _invalidate(entries: movedEntries); diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 5ca9a2466..2c8cd142a 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -5,6 +5,7 @@ import 'package:aves/model/covers.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/origins.dart'; import 'package:aves/model/favourites.dart'; +import 'package:aves/model/filters/album.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -34,6 +35,7 @@ class MediaStoreSource extends CollectionSource { if (_initState != SourceInitializationState.full) { _initState = directory != null ? SourceInitializationState.directory : SourceInitializationState.full; } + addDirectories(albums: settings.pinnedFilters.whereType().map((v) => v.album).toSet()); unawaited(_loadEntries( analysisController: analysisController, directory: directory, diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index bd8248b07..537ee0c53 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -34,8 +34,11 @@ mixin TagMixin on SourceBase { if (todo.isEmpty) return; state = SourceState.cataloguing; - var progressDone = 0; - final progressTotal = todo.length; + var progressDone = controller.progressOffset; + var progressTotal = controller.progressTotal; + if (progressTotal == 0) { + progressTotal = todo.length; + } setProgress(done: progressDone, total: progressTotal); var stopCheckCount = 0; diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index eab4c5031..25593eb39 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -210,38 +210,29 @@ class VideoMetadataFormatter { case Keys.androidCaptureFramerate: final captureFps = double.parse(value); save('Capture Frame Rate', '${roundToPrecision(captureFps, decimals: 3).toString()} FPS'); - break; case Keys.androidManufacturer: save('Android Manufacturer', value); - break; case Keys.androidModel: save('Android Model', value); - break; case Keys.androidVersion: save('Android Version', value); - break; case Keys.bitrate: case Keys.bps: save('Bit Rate', _formatMetric(value, 'b/s')); - break; case Keys.byteCount: save('Size', _formatFilesize(value)); - break; case Keys.channelLayout: save('Channel Layout', _formatChannelLayout(value)); - break; case Keys.codecName: if (value != 'none') { save('Format', _formatCodecName(value)); } - break; case Keys.codecPixelFormat: if (streamType == MediaStreamTypes.video) { // this is just a short name used by FFmpeg // user-friendly descriptions for related enums are defined in libavutil/pixfmt.h save('Pixel Format', (value as String).toUpperCase()); } - break; case Keys.codecProfileId: { final profile = int.tryParse(value); @@ -260,18 +251,14 @@ class VideoMetadataFormatter { profileString = Hevc.formatProfile(profile, level); } } - break; } case Codecs.aac: profileString = AAC.formatProfile(profile); - break; default: profileString = profile.toString(); - break; } save('Format Profile', profileString); } - break; } case Keys.compatibleBrands: final formattedBrands = RegExp(r'.{4}').allMatches(value).map((m) { @@ -279,52 +266,37 @@ class VideoMetadataFormatter { return _formatBrand(brand); }).join(', '); save('Compatible Brands', formattedBrands); - break; case Keys.creationTime: save('Creation Time', _formatDate(value)); - break; case Keys.date: if (value is String && value != '0') { final charCount = value.length; save(charCount == 4 ? 'Year' : 'Date', value); } - break; case Keys.duration: save('Duration', _formatDuration(value)); - break; case Keys.durationMicros: if (value != 0) save('Duration', formatPreciseDuration(Duration(microseconds: value))); - break; case Keys.fpsDen: save('Frame Rate', '${roundToPrecision(info[Keys.fpsNum] / info[Keys.fpsDen], decimals: 3).toString()} FPS'); - break; case Keys.frameCount: save('Frame Count', value); - break; case Keys.height: save('Height', '$value pixels'); - break; case Keys.language: if (value != 'und') save('Language', _formatLanguage(value)); - break; case Keys.location: save('Location', _formatLocation(value)); - break; case Keys.majorBrand: save('Major Brand', _formatBrand(value)); - break; case Keys.mediaFormat: save('Format', (value as String).splitMapJoin(',', onMatch: (s) => ', ', onNonMatch: _formatCodecName)); - break; case Keys.mediaType: save('Media Type', value); - break; case Keys.minorVersion: if (value != '0') save('Minor Version', value); - break; case Keys.quicktimeLocationAccuracyHorizontal: save('QuickTime Location Horizontal Accuracy', value); - break; case Keys.quicktimeCreationDate: case Keys.quicktimeLocationIso6709: case Keys.quicktimeMake: @@ -334,37 +306,27 @@ class VideoMetadataFormatter { break; case Keys.rotate: save('Rotation', '$value°'); - break; case Keys.sampleRate: save('Sample Rate', _formatMetric(value, 'Hz')); - break; case Keys.sarDen: final sarNum = info[Keys.sarNum]; final sarDen = info[Keys.sarDen]; // skip common square pixels (1:1) if (sarNum != sarDen) save('SAR', '$sarNum:$sarDen'); - break; case Keys.sourceOshash: save('Source OSHash', value); - break; case Keys.startMicros: if (value != 0) save('Start', formatPreciseDuration(Duration(microseconds: value))); - break; case Keys.statisticsWritingApp: save('Stats Writing App', value); - break; case Keys.statisticsWritingDateUtc: save('Stats Writing Date', _formatDate(value)); - break; case Keys.track: if (value != '0') save('Track', value); - break; case Keys.width: save('Width', '$value pixels'); - break; case Keys.xiaomiSlowMoment: save('Xiaomi Slow Moment', value); - break; default: save(key.toSentenceCase(), value.toString()); } diff --git a/lib/model/video/profiles/h264.dart b/lib/model/video/profiles/h264.dart index c9fe1dae6..5fb99ace3 100644 --- a/lib/model/video/profiles/h264.dart +++ b/lib/model/video/profiles/h264.dart @@ -28,43 +28,30 @@ class H264 { switch (profileIndex) { case profileBaseline: profile = 'Baseline'; - break; case profileConstrainedBaseline: profile = 'Constrained Baseline'; - break; case profileMain: profile = 'Main'; - break; case profileExtended: profile = 'Extended'; - break; case profileHigh: profile = 'High'; - break; case profileHigh10: profile = 'High 10'; - break; case profileHigh10Intra: profile = 'High 10 Intra'; - break; case profileHigh422: profile = 'High 4:2:2'; - break; case profileHigh422Intra: profile = 'High 4:2:2 Intra'; - break; case profileHigh444: profile = 'High 4:4:4'; - break; case profileHigh444Predictive: profile = 'High 4:4:4 Predictive'; - break; case profileHigh444Intra: profile = 'High 4:4:4 Intra'; - break; case profileCAVLC444: profile = 'CAVLC 4:4:4'; - break; default: return '$profileIndex'; } diff --git a/lib/model/video/profiles/hevc.dart b/lib/model/video/profiles/hevc.dart index 4678fa699..880982eef 100644 --- a/lib/model/video/profiles/hevc.dart +++ b/lib/model/video/profiles/hevc.dart @@ -9,16 +9,12 @@ class Hevc { switch (profileIndex) { case profileMain: profile = 'Main'; - break; case profileMain10: profile = 'Main 10'; - break; case profileMainStillPicture: profile = 'Main Still Picture'; - break; case profileRExt: profile = 'Format Range'; - break; default: return '$profileIndex'; } diff --git a/lib/widgets/viewer/visual/state.dart b/lib/model/view_state.dart similarity index 66% rename from lib/widgets/viewer/visual/state.dart rename to lib/model/view_state.dart index a523d7eb5..6e8c573dc 100644 --- a/lib/widgets/viewer/visual/state.dart +++ b/lib/model/view_state.dart @@ -37,4 +37,18 @@ class ViewState extends Equatable { contentSize: contentSize ?? this.contentSize, ); } + + Matrix4 get matrix { + final _viewportSize = viewportSize ?? Size.zero; + final _contentSize = contentSize ?? Size.zero; + final _scale = scale ?? 1.0; + + final scaledContentSize = _contentSize * _scale; + final viewOffset = _viewportSize.center(Offset.zero) - scaledContentSize.center(Offset.zero); + + return Matrix4.identity() + ..translate(position.dx, position.dy) + ..translate(viewOffset.dx, viewOffset.dy) + ..scale(_scale, _scale, 1); + } } diff --git a/lib/ref/poi.dart b/lib/ref/poi.dart index 1aebe9aae..951239d39 100644 --- a/lib/ref/poi.dart +++ b/lib/ref/poi.dart @@ -12,4 +12,4 @@ class PointsOfInterest { LatLng(37.637861, 21.63), LatLng(37.949722, 27.363889), ]; -} \ No newline at end of file +} diff --git a/lib/services/analysis_service.dart b/lib/services/analysis_service.dart index dde0d5675..071561f35 100644 --- a/lib/services/analysis_service.dart +++ b/lib/services/analysis_service.dart @@ -31,7 +31,7 @@ class AnalysisService { static Future startService({required bool force, List? entryIds}) async { await reportService.log('Start analysis service${entryIds != null ? ' for ${entryIds.length} items' : ''}'); try { - await _platform.invokeMethod('startService', { + await _platform.invokeMethod('startAnalysis', { 'entryIds': entryIds, 'force': force, }); @@ -108,15 +108,20 @@ class Analyzer { Future start(dynamic args) async { List? entryIds; var force = false; + var progressTotal = 0, progressOffset = 0; if (args is Map) { entryIds = (args['entryIds'] as List?)?.cast(); force = args['force'] ?? false; + progressTotal = args['progressTotal']; + progressOffset = args['progressOffset']; } - debugPrint('$runtimeType start for ${entryIds?.length ?? 'all'} entries'); + debugPrint('$runtimeType start for ${entryIds?.length ?? 'all'} entries, at $progressOffset/$progressTotal'); _controller = AnalysisController( canStartService: false, entryIds: entryIds, force: force, + progressTotal: progressTotal, + progressOffset: progressOffset, stopSignal: ValueNotifier(false), ); @@ -145,17 +150,14 @@ class Analyzer { case AnalyzerState.stopping: await _stopPlatformService(); _serviceStateNotifier.value = AnalyzerState.stopped; - break; case AnalyzerState.stopped: _controller?.stopSignal.value = true; _stopUpdateTimer(); - break; } } void _onSourceStateChanged() { if (_source.isReady) { - _refreshApp(); _serviceStateNotifier.value = AnalyzerState.stopping; } } @@ -179,14 +181,6 @@ class Analyzer { } } - Future _refreshApp() async { - try { - await _channel.invokeMethod('refreshApp'); - } on PlatformException catch (e, stack) { - await reportService.recordError(e, stack); - } - } - Future _stopPlatformService() async { try { await _channel.invokeMethod('stop'); diff --git a/lib/services/media/media_session_service.dart b/lib/services/media/media_session_service.dart index 8280965c7..2d287f910 100644 --- a/lib/services/media/media_session_service.dart +++ b/lib/services/media/media_session_service.dart @@ -96,25 +96,19 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable { switch (command) { case 'play': event = const MediaCommandEvent(MediaCommand.play); - break; case 'pause': event = const MediaCommandEvent(MediaCommand.pause); - break; case 'skip_to_next': event = const MediaCommandEvent(MediaCommand.skipToNext); - break; case 'skip_to_previous': event = const MediaCommandEvent(MediaCommand.skipToPrevious); - break; case 'stop': event = const MediaCommandEvent(MediaCommand.stop); - break; case 'seek': final position = fields['position'] as int?; if (position != null) { event = MediaSeekCommandEvent(MediaCommand.stop, position: position); } - break; } if (event != null) { _streamController.add(event); diff --git a/lib/services/metadata/metadata_edit_service.dart b/lib/services/metadata/metadata_edit_service.dart index ec3d48bac..4eeecbfed 100644 --- a/lib/services/metadata/metadata_edit_service.dart +++ b/lib/services/metadata/metadata_edit_service.dart @@ -6,8 +6,10 @@ import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/metadata/date_modifier.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves_model/aves_model.dart'; +import 'package:aves_report/aves_report.dart'; import 'package:collection/collection.dart'; import 'package:flutter/services.dart'; +import 'package:stack_trace/stack_trace.dart'; abstract class MetadataEditService { Future> rotate(AvesEntry entry, {required bool clockwise}); @@ -125,44 +127,52 @@ class PlatformMetadataEditService implements MetadataEditService { Future _processPlatformException(AvesEntry entry, PlatformException e, StackTrace stack) async { if (entry.isValid) { final code = e.code; + final customException = CustomPlatformException.fromStandard(e); if (code.endsWith('mp4largemoov')) { - await reportService.recordError(_Mp4LargeMoovException(code: e.code, message: e.message, details: e.details, stacktrace: e.stacktrace), stack); + await mp4LargeMoov(customException); } else if (code.endsWith('mp4largeother')) { - await reportService.recordError(_Mp4LargeOtherException(code: e.code, message: e.message, details: e.details, stacktrace: e.stacktrace), stack); + await mp4LargeOther(customException); } else if (code.endsWith('filenotfound')) { - await reportService.recordError(_FileNotFoundException(code: e.code, message: e.message, details: e.details, stacktrace: e.stacktrace), stack); + await fileNotFound(customException); } else { await reportService.recordError(e, stack); } } } + + StackTrace? _currentStack() => ReportService.buildReportStack(Trace.current(), level: 1); + + // distinct exceptions to convince Crashlytics to split reports into distinct issues + + Future mp4LargeMoov(CustomPlatformException e) => reportService.recordError(e, _currentStack()); + + Future mp4LargeOther(CustomPlatformException e) => reportService.recordError(e, _currentStack()); + + Future fileNotFound(CustomPlatformException e) => reportService.recordError(e, _currentStack()); } -// distinct exceptions to convince Crashlytics to split reports into distinct issues +class CustomPlatformException { + final String code; + final String? message; + final dynamic details; + final String? stacktrace; -class _Mp4LargeMoovException extends PlatformException { - _Mp4LargeMoovException({ - required super.code, - required super.message, - required super.details, - required super.stacktrace, + CustomPlatformException({ + required this.code, + this.message, + this.details, + this.stacktrace, }); -} -class _Mp4LargeOtherException extends PlatformException { - _Mp4LargeOtherException({ - required super.code, - required super.message, - required super.details, - required super.stacktrace, - }); -} + factory CustomPlatformException.fromStandard(PlatformException e) { + return CustomPlatformException( + code: e.code, + message: e.message, + details: e.details, + stacktrace: e.stacktrace, + ); + } -class _FileNotFoundException extends PlatformException { - _FileNotFoundException({ - required super.code, - required super.message, - required super.details, - required super.stacktrace, - }); + @override + String toString() => '$runtimeType($code, $message, $details, $stacktrace)'; } diff --git a/lib/services/metadata/svg_metadata_service.dart b/lib/services/metadata/svg_metadata_service.dart index 677bb97dc..0567b1908 100644 --- a/lib/services/metadata/svg_metadata_service.dart +++ b/lib/services/metadata/svg_metadata_service.dart @@ -80,7 +80,7 @@ class SvgMetadataService { final docDir = Map.fromEntries([ ...root.attributes.where((a) => _attributes.contains(a.name.qualified)).map((a) => MapEntry(formatKey(a.name.qualified), a.value)), ..._textElements.map((name) { - final value = root.getElement(name)?.text; + final value = root.getElement(name)?.innerText; return value != null ? MapEntry(formatKey(name), value) : null; }).whereNotNull(), ]); diff --git a/lib/services/window_service.dart b/lib/services/window_service.dart index 81796bff8..44f9a9d73 100644 --- a/lib/services/window_service.dart +++ b/lib/services/window_service.dart @@ -74,15 +74,12 @@ class PlatformWindowService implements WindowService { case Orientation.landscape: // SCREEN_ORIENTATION_SENSOR_LANDSCAPE orientationCode = 6; - break; case Orientation.portrait: // SCREEN_ORIENTATION_SENSOR_PORTRAIT orientationCode = 7; - break; default: // SCREEN_ORIENTATION_UNSPECIFIED orientationCode = -1; - break; } try { await _platform.invokeMethod('requestOrientation', { diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 4c683e908..8f46027fa 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -2,172 +2,180 @@ import 'package:flutter/material.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class AIcons { - static const IconData allCollection = Icons.collections_outlined; - static const IconData image = Icons.photo_outlined; - static const IconData video = Icons.movie_outlined; - static const IconData vector = Icons.code_outlined; + static const allCollection = Icons.collections_outlined; + static const image = Icons.photo_outlined; + static const video = Icons.movie_outlined; + static const vector = Icons.code_outlined; - static const IconData accessibility = Icons.accessibility_new_outlined; - static const IconData android = Icons.android; - static const IconData app = Icons.apps_outlined; - static const IconData apply = Icons.done_outlined; - static const IconData aspectRatio = Icons.aspect_ratio_outlined; - static const IconData bin = Icons.delete_outlined; - static const IconData broken = Icons.broken_image_outlined; - static const IconData brightnessMin = Icons.brightness_low_outlined; - static const IconData brightnessMax = Icons.brightness_high_outlined; - static const IconData checked = Icons.done_outlined; - static const IconData count = MdiIcons.counter; - static const IconData counter = Icons.plus_one_outlined; - static const IconData date = Icons.calendar_today_outlined; - static const IconData dateByDay = Icons.today_outlined; - static const IconData dateByMonth = Icons.calendar_month_outlined; - static const IconData dateRecent = Icons.today_outlined; - static const IconData dateUndated = Icons.event_busy_outlined; - static const IconData description = Icons.description_outlined; - static const IconData descriptionUntitled = Icons.comments_disabled_outlined; - static const IconData disc = Icons.fiber_manual_record; - static const IconData display = Icons.light_mode_outlined; - static const IconData error = Icons.error_outline; - static const IconData folder = Icons.folder_outlined; - static const IconData grid = Icons.grid_on_outlined; - static const IconData home = Icons.home_outlined; - static const IconData important = Icons.label_important_outline; - static const IconData language = Icons.translate_outlined; - static const IconData location = Icons.place_outlined; - static const IconData locationUnlocated = Icons.location_off_outlined; - static const IconData country = Icons.flag_outlined; - static const IconData state = Icons.flag_outlined; - static const IconData place = Icons.place_outlined; - static const IconData mainStorage = Icons.smartphone_outlined; - static const IconData mimeType = Icons.code_outlined; - static const IconData opacity = Icons.opacity; - static const IconData privacy = MdiIcons.shieldAccountOutline; - static const IconData rating = Icons.star_border_outlined; - static const IconData ratingFull = Icons.star; - static const IconData ratingRejected = MdiIcons.starMinusOutline; - static const IconData ratingUnrated = MdiIcons.starOffOutline; - static const IconData raw = Icons.raw_on_outlined; - static const IconData shooting = Icons.camera_outlined; - static const IconData removableStorage = Icons.sd_storage_outlined; - static const IconData sensorControlEnabled = Icons.explore_outlined; - static const IconData sensorControlDisabled = Icons.explore_off_outlined; - static const IconData settings = Icons.settings_outlined; - static const IconData size = Icons.data_usage_outlined; - static const IconData text = Icons.format_quote_outlined; - static const IconData tag = Icons.local_offer_outlined; - static const IconData tagUntagged = MdiIcons.tagOffOutline; - static const IconData volumeMin = Icons.volume_mute_outlined; - static const IconData volumeMax = Icons.volume_up_outlined; + static const accessibility = Icons.accessibility_new_outlined; + static const android = Icons.android; + static const app = Icons.apps_outlined; + static const apply = Icons.done_outlined; + static const aspectRatio = Icons.aspect_ratio_outlined; + static const bin = Icons.delete_outlined; + static const broken = Icons.broken_image_outlined; + static const brightnessMin = Icons.brightness_low_outlined; + static const brightnessMax = Icons.brightness_high_outlined; + static const checked = Icons.done_outlined; + static const count = MdiIcons.counter; + static const counter = Icons.plus_one_outlined; + static const date = Icons.calendar_today_outlined; + static const dateByDay = Icons.today_outlined; + static const dateByMonth = Icons.calendar_month_outlined; + static const dateRecent = Icons.today_outlined; + static const dateUndated = Icons.event_busy_outlined; + static const description = Icons.description_outlined; + static const descriptionUntitled = Icons.comments_disabled_outlined; + static const disc = Icons.fiber_manual_record; + static const display = Icons.light_mode_outlined; + static const error = Icons.error_outline; + static const folder = Icons.folder_outlined; + static const grid = Icons.grid_on_outlined; + static const home = Icons.home_outlined; + static const important = Icons.label_important_outline; + static const language = Icons.translate_outlined; + static const location = Icons.place_outlined; + static const locationUnlocated = Icons.location_off_outlined; + static const country = Icons.flag_outlined; + static const state = Icons.flag_outlined; + static const place = Icons.place_outlined; + static const mainStorage = Icons.smartphone_outlined; + static const mimeType = Icons.code_outlined; + static const opacity = Icons.opacity; + static const privacy = MdiIcons.shieldAccountOutline; + static const rating = Icons.star_border_outlined; + static const ratingFull = Icons.star; + static const ratingRejected = MdiIcons.starMinusOutline; + static const ratingUnrated = MdiIcons.starOffOutline; + static const raw = Icons.raw_on_outlined; + static const shooting = Icons.camera_outlined; + static const removableStorage = Icons.sd_storage_outlined; + static const sensorControlEnabled = Icons.explore_outlined; + static const sensorControlDisabled = Icons.explore_off_outlined; + static const settings = Icons.settings_outlined; + static const size = Icons.data_usage_outlined; + static const text = Icons.format_quote_outlined; + static const tag = Icons.local_offer_outlined; + static const tagUntagged = MdiIcons.tagOffOutline; + static const volumeMin = Icons.volume_mute_outlined; + static const volumeMax = Icons.volume_up_outlined; // view - static const IconData group = Icons.group_work_outlined; - static const IconData layout = Icons.grid_view_outlined; - static const IconData layoutMosaic = Icons.view_comfy_outlined; - static const IconData layoutGrid = Icons.view_compact_outlined; - static const IconData layoutList = Icons.list_outlined; - static const IconData sort = Icons.sort_outlined; - static const IconData sortOrder = Icons.swap_vert_outlined; - static const IconData thumbnailLarge = Icons.photo_size_select_large_outlined; - static const IconData thumbnailSmall = Icons.photo_size_select_small_outlined; + static const group = Icons.group_work_outlined; + static const layout = Icons.grid_view_outlined; + static const layoutMosaic = Icons.view_comfy_outlined; + static const layoutGrid = Icons.view_compact_outlined; + static const layoutList = Icons.list_outlined; + static const sort = Icons.sort_outlined; + static const sortOrder = Icons.swap_vert_outlined; + static const thumbnailLarge = Icons.photo_size_select_large_outlined; + static const thumbnailSmall = Icons.photo_size_select_small_outlined; // actions - static const IconData add = Icons.add_circle_outline; - static const IconData addShortcut = Icons.add_to_home_screen_outlined; - static const IconData cancel = Icons.cancel_outlined; - static const IconData captureFrame = Icons.screenshot_outlined; - static const IconData clear = Icons.clear_outlined; - static const IconData clipboard = Icons.content_copy_outlined; - static const IconData convert = Icons.transform_outlined; - static const IconData convertToStillImage = MdiIcons.movieRemoveOutline; - static const IconData copy = Icons.file_copy_outlined; - static const IconData debug = Icons.whatshot_outlined; - static const IconData delete = Icons.delete_outlined; - static const IconData edit = Icons.edit_outlined; - static const IconData emptyBin = Icons.delete_sweep_outlined; - static const IconData export = Icons.open_with_outlined; - static const IconData fileExport = MdiIcons.fileExportOutline; - static const IconData fileImport = MdiIcons.fileImportOutline; - static const IconData flip = Icons.flip_outlined; - static const IconData favourite = Icons.favorite_border; - static const IconData favouriteActive = Icons.favorite; - static const IconData filter = MdiIcons.filterOutline; - static const IconData filterOff = MdiIcons.filterOffOutline; - static const IconData geoBounds = Icons.public_outlined; - static const IconData goUp = Icons.arrow_upward_outlined; - static const IconData hide = Icons.visibility_off_outlined; - static const IconData info = Icons.info_outlined; - static const IconData layers = Icons.layers_outlined; - static const IconData map = Icons.map_outlined; - static const IconData move = MdiIcons.fileMoveOutline; - static const IconData mute = Icons.volume_off_outlined; - static const IconData unmute = Icons.volume_up_outlined; - static const IconData name = Icons.abc_outlined; - static const IconData newTier = Icons.fiber_new_outlined; - static const IconData openOutside = Icons.open_in_new_outlined; - static const IconData openVideo = MdiIcons.moviePlayOutline; - static const IconData pin = Icons.push_pin_outlined; - static const IconData unpin = MdiIcons.pinOffOutline; - static const IconData play = Icons.play_arrow; - static const IconData pause = Icons.pause; - static const IconData print = Icons.print_outlined; - static const IconData refresh = Icons.refresh_outlined; - static const IconData replay10 = Icons.replay_10_outlined; - static const IconData reverse = Icons.invert_colors_outlined; - static const IconData skip10 = Icons.forward_10_outlined; - static const IconData reset = Icons.restart_alt_outlined; - static const IconData restore = Icons.restore_outlined; - static const IconData rotateLeft = Icons.rotate_left_outlined; - static const IconData rotateRight = Icons.rotate_right_outlined; - static const IconData rotateScreen = Icons.screen_rotation_outlined; - static const IconData search = Icons.search_outlined; - static const IconData select = Icons.select_all_outlined; - static const IconData setAs = Icons.wallpaper_outlined; - static const IconData setCover = MdiIcons.imageEditOutline; - static const IconData share = Icons.share_outlined; - static const IconData show = Icons.visibility_outlined; - static const IconData showFullscreen = MdiIcons.arrowExpand; - static const IconData slideshow = Icons.slideshow_outlined; - static const IconData speed = Icons.speed_outlined; - static const IconData stats = Icons.donut_small_outlined; - static const IconData streams = Icons.translate_outlined; - static const IconData streamVideo = Icons.movie_outlined; - static const IconData streamAudio = Icons.audiotrack_outlined; - static const IconData streamText = Icons.closed_caption_outlined; - static const IconData vaultLock = Icons.lock_outline; - static const IconData vaultAdd = Icons.enhanced_encryption_outlined; - static const IconData vaultConfigure = MdiIcons.shieldLockOutline; - static const IconData videoSettings = Icons.video_settings_outlined; - static const IconData view = Icons.grid_view_outlined; - static const IconData viewerLock = Icons.lock_outline; - static const IconData viewerUnlock = Icons.lock_open_outlined; - static const IconData zoomIn = Icons.add_outlined; - static const IconData zoomOut = Icons.remove_outlined; - static const IconData collapse = Icons.expand_less_outlined; - static const IconData expand = Icons.expand_more_outlined; - static const IconData previous = Icons.chevron_left_outlined; - static const IconData next = Icons.chevron_right_outlined; + static const add = Icons.add_circle_outline; + static const addShortcut = Icons.add_to_home_screen_outlined; + static const cancel = Icons.cancel_outlined; + static const captureFrame = Icons.screenshot_outlined; + static const clear = Icons.clear_outlined; + static const clipboard = Icons.content_copy_outlined; + static const convert = Icons.transform_outlined; + static const convertToStillImage = MdiIcons.movieRemoveOutline; + static const copy = Icons.file_copy_outlined; + static const debug = Icons.whatshot_outlined; + static const delete = Icons.delete_outlined; + static const edit = Icons.edit_outlined; + static const emptyBin = Icons.delete_sweep_outlined; + static const export = Icons.open_with_outlined; + static const fileExport = MdiIcons.fileExportOutline; + static const fileImport = MdiIcons.fileImportOutline; + static const flip = Icons.flip_outlined; + static const favourite = Icons.favorite_border; + static const favouriteActive = Icons.favorite; + static const filter = MdiIcons.filterOutline; + static const filterOff = MdiIcons.filterOffOutline; + static const geoBounds = Icons.public_outlined; + static const goUp = Icons.arrow_upward_outlined; + static const hide = Icons.visibility_off_outlined; + static const info = Icons.info_outlined; + static const layers = Icons.layers_outlined; + static const map = Icons.map_outlined; + static const move = MdiIcons.fileMoveOutline; + static const mute = Icons.volume_off_outlined; + static const unmute = Icons.volume_up_outlined; + static const name = Icons.abc_outlined; + static const newTier = Icons.fiber_new_outlined; + static const openOutside = Icons.open_in_new_outlined; + static const openVideo = MdiIcons.moviePlayOutline; + static const pin = Icons.push_pin_outlined; + static const unpin = MdiIcons.pinOffOutline; + static const play = Icons.play_arrow; + static const pause = Icons.pause; + static const print = Icons.print_outlined; + static const refresh = Icons.refresh_outlined; + static const replay10 = Icons.replay_10_outlined; + static const reverse = Icons.invert_colors_outlined; + static const skip10 = Icons.forward_10_outlined; + static const reset = Icons.restart_alt_outlined; + static const restore = Icons.restore_outlined; + static const rotateLeft = Icons.rotate_left_outlined; + static const rotateRight = Icons.rotate_right_outlined; + static const rotateScreen = Icons.screen_rotation_outlined; + static const search = Icons.search_outlined; + static const select = Icons.select_all_outlined; + static const setAs = Icons.wallpaper_outlined; + static const setCover = MdiIcons.imageEditOutline; + static const share = Icons.share_outlined; + static const show = Icons.visibility_outlined; + static const showFullscreen = MdiIcons.arrowExpand; + static const slideshow = Icons.slideshow_outlined; + static const speed = Icons.speed_outlined; + static const stats = Icons.donut_small_outlined; + static const streams = Icons.translate_outlined; + static const streamVideo = Icons.movie_outlined; + static const streamAudio = Icons.audiotrack_outlined; + static const streamText = Icons.closed_caption_outlined; + static const vaultLock = Icons.lock_outline; + static const vaultAdd = Icons.enhanced_encryption_outlined; + static const vaultConfigure = MdiIcons.shieldLockOutline; + static const videoSettings = Icons.video_settings_outlined; + static const view = Icons.grid_view_outlined; + static const viewerLock = Icons.lock_outline; + static const viewerUnlock = Icons.lock_open_outlined; + static const zoomIn = Icons.add_outlined; + static const zoomOut = Icons.remove_outlined; + static const collapse = Icons.expand_less_outlined; + static const expand = Icons.expand_more_outlined; + static const previous = Icons.chevron_left_outlined; + static const next = Icons.chevron_right_outlined; + + // editor + static const transform = Icons.crop_rotate_outlined; + static const aspectRatioFree = Icons.crop_free_outlined; + static const aspectRatioOriginal = Icons.crop_original_outlined; + static const aspectRatioSquare = Icons.crop_square_outlined; + static const aspectRatio_16_9 = Icons.crop_16_9_outlined; + static const aspectRatio_4_3 = Icons.crop_landscape_outlined; // albums - static const IconData album = Icons.photo_album_outlined; - static const IconData cameraAlbum = Icons.photo_camera_outlined; - static const IconData downloadAlbum = Icons.file_download; - static const IconData screenshotAlbum = Icons.screenshot_outlined; - static const IconData recordingAlbum = Icons.smartphone_outlined; - static const IconData locked = Icons.lock_outline; - static const IconData unlocked = Icons.lock_open_outlined; + static const album = Icons.photo_album_outlined; + static const cameraAlbum = Icons.photo_camera_outlined; + static const downloadAlbum = Icons.file_download; + static const screenshotAlbum = Icons.screenshot_outlined; + static const recordingAlbum = Icons.smartphone_outlined; + static const locked = Icons.lock_outline; + static const unlocked = Icons.lock_open_outlined; // thumbnail overlay - static const IconData animated = Icons.slideshow; - static const IconData geo = Icons.language_outlined; - static const IconData motionPhoto = Icons.motion_photos_on_outlined; - static const IconData multiPage = Icons.burst_mode_outlined; - static const IconData panorama = Icons.vrpano_outlined; - static const IconData sphericalVideo = Icons.threesixty_outlined; - static const IconData videoThumb = Icons.play_circle_outline; - static const IconData selected = Icons.check_circle_outline; - static const IconData unselected = Icons.radio_button_unchecked; + static const animated = Icons.slideshow; + static const geo = Icons.language_outlined; + static const motionPhoto = Icons.motion_photos_on_outlined; + static const multiPage = Icons.burst_mode_outlined; + static const panorama = Icons.vrpano_outlined; + static const sphericalVideo = Icons.threesixty_outlined; + static const videoThumb = Icons.play_circle_outline; + static const selected = Icons.check_circle_outline; + static const unselected = Icons.radio_button_unchecked; - static const IconData github = MdiIcons.github; - static const IconData legal = MdiIcons.scaleBalance; + static const github = MdiIcons.github; + static const legal = MdiIcons.scaleBalance; } diff --git a/lib/theme/text.dart b/lib/theme/text.dart index b7840d33f..27501eb06 100644 --- a/lib/theme/text.dart +++ b/lib/theme/text.dart @@ -4,4 +4,4 @@ class AText { static const separator = ' ${UniChars.bullet} '; static const resolutionSeparator = ' ${UniChars.multiplicationSign} '; static const valueNotAvailable = UniChars.emDash; -} \ No newline at end of file +} diff --git a/lib/utils/diff_match.dart b/lib/utils/diff_match.dart index 29042c4de..4c4a7b9ef 100644 --- a/lib/utils/diff_match.dart +++ b/lib/utils/diff_match.dart @@ -274,11 +274,9 @@ class DiffMatchPatch { case Operation.insert: count_insert++; text_insert.write(diffs[pointer].text); - break; case Operation.delete: count_delete++; text_delete.write(diffs[pointer].text); - break; case Operation.equal: // Upon reaching an equality, check for prior redundancies. if (count_delete >= 1 && count_insert >= 1) { @@ -295,7 +293,6 @@ class DiffMatchPatch { count_delete = 0; text_delete.clear(); text_insert.clear(); - break; } pointer++; } @@ -1013,12 +1010,10 @@ class DiffMatchPatch { count_insert++; text_insert += diffs[pointer].text; pointer++; - break; case Operation.delete: count_delete++; text_delete += diffs[pointer].text; pointer++; - break; case Operation.equal: // Upon reaching an equality, check for prior redundancies. if (count_delete + count_insert > 1) { @@ -1068,7 +1063,6 @@ class DiffMatchPatch { count_delete = 0; text_delete = ''; text_insert = ''; - break; } } if (diffs.last.text.isEmpty) { @@ -1155,17 +1149,14 @@ class DiffMatchPatch { html.write(''); html.write(text); html.write(''); - break; case Operation.delete: html.write(''); html.write(text); html.write(''); - break; case Operation.equal: html.write(''); html.write(text); html.write(''); - break; } } return html.toString(); @@ -1209,16 +1200,13 @@ class DiffMatchPatch { switch (aDiff.operation) { case Operation.insert: insertions += aDiff.text.length; - break; case Operation.delete: deletions += aDiff.text.length; - break; case Operation.equal: // A deletion and an insertion is one substitution. levenshtein += max(insertions, deletions); insertions = 0; deletions = 0; - break; } } levenshtein += max(insertions, deletions); @@ -1239,17 +1227,14 @@ class DiffMatchPatch { text.write('+'); text.write(Uri.encodeFull(aDiff.text)); text.write('\t'); - break; case Operation.delete: text.write('-'); text.write(aDiff.text.length); text.write('\t'); - break; case Operation.equal: text.write('='); text.write(aDiff.text.length); text.write('\t'); - break; } } String delta = text.toString(); @@ -1289,7 +1274,6 @@ class DiffMatchPatch { throw ArgumentError('Illegal escape in diff_fromDelta: $param'); } diffs.add(Diff(Operation.insert, param)); - break; case '-': // Fall through. case '=': @@ -1314,7 +1298,6 @@ class DiffMatchPatch { } else { diffs.add(Diff(Operation.delete, text)); } - break; default: // Anything else is an error. throw ArgumentError('Invalid diff operation in diff_fromDelta: ${token[0]}'); diff --git a/lib/utils/math_utils.dart b/lib/utils/math_utils.dart index 89a0e6ed5..e882db4aa 100644 --- a/lib/utils/math_utils.dart +++ b/lib/utils/math_utils.dart @@ -1,7 +1,48 @@ import 'dart:math'; +import 'dart:ui'; + +import 'package:tuple/tuple.dart'; int highestPowerOf2(num x) => x < 1 ? 0 : pow(2, (log(x) / ln2).floor()).toInt(); int smallestPowerOf2(num x) => x < 1 ? 1 : pow(2, (log(x) / ln2).ceil()).toInt(); double roundToPrecision(final double value, {required final int decimals}) => (value * pow(10, decimals)).round() / pow(10, decimals); + +// cf https://en.wikipedia.org/wiki/Intersection_(geometry)#Two_line_segments +Offset? segmentIntersection(Tuple2 s1, Tuple2 s2) { + final x1 = s1.item1.dx; + final y1 = s1.item1.dy; + final x2 = s1.item2.dx; + final y2 = s1.item2.dy; + + final x3 = s2.item1.dx; + final y3 = s2.item1.dy; + final x4 = s2.item2.dx; + final y4 = s2.item2.dy; + + final a1 = x2 - x1; + final b1 = -(x4 - x3); + final c1 = x3 - x1; + final a2 = y2 - y1; + final b2 = -(y4 - y3); + final c2 = y3 - y1; + + final denom = a1 * b2 - a2 * b1; + if (denom == 0) { + // lines are parallel + return null; + } + + final s0 = (c1 * b2 - c2 * b1) / denom; + final t0 = (a1 * c2 - a2 * c1) / denom; + + if (!(0 <= s0 && s0 <= 1 && 0 <= t0 && t0 <= 1)) { + // segments do not intersect + return null; + } + + final x0 = x1 + s0 * (x2 - x1); + final y0 = y1 + s0 * (y2 - y1); + return Offset(x0, y0); +} diff --git a/lib/utils/xmp_utils.dart b/lib/utils/xmp_utils.dart index c46274fa7..0953bac31 100644 --- a/lib/utils/xmp_utils.dart +++ b/lib/utils/xmp_utils.dart @@ -26,7 +26,7 @@ class XMP { static const nsXmp = XmpNamespaces.xmp; // for `rdf:Description` node only - static bool _hasMeaningfulChildren(XmlNode node) => node.children.any((v) => v.nodeType != XmlNodeType.TEXT || v.text.trim().isNotEmpty); + static bool _hasMeaningfulChildren(XmlNode node) => node.children.any((v) => v.nodeType != XmlNodeType.TEXT || v.innerText.trim().isNotEmpty); // for `rdf:Description` node only static bool _hasMeaningfulAttributes(XmlNode description) { diff --git a/lib/view/src/editor/enums.dart b/lib/view/src/editor/enums.dart new file mode 100644 index 000000000..43b9fe95d --- /dev/null +++ b/lib/view/src/editor/enums.dart @@ -0,0 +1,57 @@ +import 'package:aves/ref/unicode.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/widgets.dart'; + +extension ExtraEditorActionView on EditorAction { + String getText(BuildContext context) { + switch (this) { + case EditorAction.transform: + return context.l10n.editorActionTransform; + } + } + + Widget getIcon() => Icon(_getIconData()); + + IconData _getIconData() { + switch (this) { + case EditorAction.transform: + return AIcons.transform; + } + } +} + +extension ExtraCropAspectRatioView on CropAspectRatio { + String getText(BuildContext context) { + switch (this) { + case CropAspectRatio.free: + return context.l10n.cropAspectRatioFree; + case CropAspectRatio.original: + return context.l10n.cropAspectRatioOriginal; + case CropAspectRatio.square: + return context.l10n.cropAspectRatioSquare; + case CropAspectRatio.ar_16_9: + return '16${UniChars.ratio}9'; + case CropAspectRatio.ar_4_3: + return '4${UniChars.ratio}3'; + } + } + + Widget getIcon() => Icon(_getIconData()); + + IconData _getIconData() { + switch (this) { + case CropAspectRatio.free: + return AIcons.aspectRatioFree; + case CropAspectRatio.original: + return AIcons.aspectRatioOriginal; + case CropAspectRatio.square: + return AIcons.aspectRatioSquare; + case CropAspectRatio.ar_16_9: + return AIcons.aspectRatio_16_9; + case CropAspectRatio.ar_4_3: + return AIcons.aspectRatio_4_3; + } + } +} diff --git a/lib/view/src/settings/enums.dart b/lib/view/src/settings/enums.dart index 642d1c305..8b7abc0c5 100644 --- a/lib/view/src/settings/enums.dart +++ b/lib/view/src/settings/enums.dart @@ -104,6 +104,8 @@ extension ExtraHomePageSettingView on HomePageSetting { return context.l10n.drawerCollectionAll; case HomePageSetting.albums: return context.l10n.drawerAlbumPage; + case HomePageSetting.tags: + return context.l10n.drawerTagPage; } } } @@ -249,6 +251,8 @@ extension ExtraViewerTransitionView on ViewerTransition { return context.l10n.viewerTransitionZoomIn; case ViewerTransition.none: return context.l10n.viewerTransitionNone; + case ViewerTransition.random: + return context.l10n.widgetDisplayedItemRandom; } } } @@ -273,6 +277,8 @@ extension ExtraWidgetOpenPageView on WidgetOpenPage { return context.l10n.widgetOpenPageCollection; case WidgetOpenPage.viewer: return context.l10n.widgetOpenPageViewer; + case WidgetOpenPage.updateWidget: + return context.l10n.widgetTapUpdateWidget; } } } diff --git a/lib/view/view.dart b/lib/view/view.dart index 76eec0d8c..fd3cfd0ff 100644 --- a/lib/view/view.dart +++ b/lib/view/view.dart @@ -6,6 +6,7 @@ export 'src/actions/map.dart'; export 'src/actions/map_cluster.dart'; export 'src/actions/share.dart'; export 'src/actions/slideshow.dart'; +export 'src/editor/enums.dart'; export 'src/metadata/date_edit_action.dart'; export 'src/metadata/date_field_source.dart'; export 'src/metadata/fields.dart'; diff --git a/lib/widget_common.dart b/lib/widget_common.dart index fe14fee12..4197f0311 100644 --- a/lib/widget_common.dart +++ b/lib/widget_common.dart @@ -34,7 +34,7 @@ void widgetMainCommon(AppFlavor flavor) async { }); } -Future _drawWidget(dynamic args) async { +Future> _drawWidget(dynamic args) async { final widgetId = args['widgetId'] as int; final widthPx = args['widthPx'] as int; final heightPx = args['heightPx'] as int; @@ -47,12 +47,16 @@ Future _drawWidget(dynamic args) async { entry: entry, devicePixelRatio: devicePixelRatio, ); - return painter.drawWidget( + final bytes = await painter.drawWidget( widthPx: widthPx, heightPx: heightPx, outline: settings.getWidgetOutline(widgetId), shape: settings.getWidgetShape(widgetId), ); + return { + 'bytes': bytes, + 'updateOnTap': settings.getWidgetOpenPage(widgetId) == WidgetOpenPage.updateWidget, + }; } Future _getWidgetEntry(int widgetId, bool reuseEntry) async { @@ -79,10 +83,8 @@ Future _getWidgetEntry(int widgetId, bool reuseEntry) async { switch (settings.getWidgetDisplayedItem(widgetId)) { case WidgetDisplayedItem.random: entries.shuffle(); - break; case WidgetDisplayedItem.mostRecent: entries.sort(AvesEntrySort.compareByDate); - break; } final entry = entries.firstOrNull; if (entry != null) { diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart index 83c6000d0..af20e6d55 100644 --- a/lib/widgets/about/app_ref.dart +++ b/lib/widgets/about/app_ref.dart @@ -94,11 +94,12 @@ class _AppReferenceState extends State { return FutureBuilder( future: _packageInfoLoader, builder: (context, snapshot) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); return Row( mainAxisSize: MainAxisSize.min, children: [ AvesLogo( - size: _appTitleStyle.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.3, + size: _appTitleStyle.fontSize! * textScaleFactor * 1.3, ), const SizedBox(width: 8), Text( diff --git a/lib/widgets/about/bug_report.dart b/lib/widgets/about/bug_report.dart index 954146ea2..6cb94eb44 100644 --- a/lib/widgets/about/bug_report.dart +++ b/lib/widgets/about/bug_report.dart @@ -157,7 +157,7 @@ class _BugReportState extends State with FeedbackMixin { 'Device: ${androidInfo.manufacturer} ${androidInfo.model}', 'Geocoder: ${device.hasGeocoder ? 'ready' : 'not available'}', 'Mobile services: ${mobileServices.isServiceAvailable ? 'ready' : 'not available'}', - 'System locales: ${WidgetsBinding.instance.window.locales.join(', ')}', + 'System locales: ${WidgetsBinding.instance.platformDispatcher.locales.join(', ')}', 'Storage volumes: ${storageVolumes.map((v) => v.path).join(', ')}', 'Storage grants: ${storageGrants.join(', ')}', 'Error reporting: ${settings.isErrorReportingAllowed}', diff --git a/lib/widgets/about/tv_license_page.dart b/lib/widgets/about/tv_license_page.dart index fa4e2b7e2..ef229b3e9 100644 --- a/lib/widgets/about/tv_license_page.dart +++ b/lib/widgets/about/tv_license_page.dart @@ -1,7 +1,9 @@ +import 'dart:developer' show Flow, Timeline; + import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/behaviour/intents.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Flow; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; @@ -209,10 +211,21 @@ class _PackageLicensePageState extends State<_PackageLicensePage> { bool _loaded = false; Future _initLicenses() async { + int debugFlowId = -1; + assert(() { + final Flow flow = Flow.begin(); + Timeline.timeSync('_initLicenses()', () {}, flow: flow); + debugFlowId = flow.id; + return true; + }()); for (final LicenseEntry license in widget.licenseEntries) { if (!mounted) { return; } + assert(() { + Timeline.timeSync('_initLicenses()', () {}, flow: Flow.step(debugFlowId)); + return true; + }()); final List paragraphs = await SchedulerBinding.instance.scheduleTask>( license.paragraphs.toList, Priority.animation, @@ -237,6 +250,7 @@ class _PackageLicensePageState extends State<_PackageLicensePage> { ), )); } else { + assert(paragraph.indent >= 0); _licenses.add(Padding( padding: EdgeInsetsDirectional.only(top: 8.0, start: 16.0 * paragraph.indent), child: Text(paragraph.text), @@ -248,16 +262,21 @@ class _PackageLicensePageState extends State<_PackageLicensePage> { setState(() { _loaded = true; }); + assert(() { + Timeline.timeSync('Build scheduled', () {}, flow: Flow.end(debugFlowId)); + return true; + }()); } @override Widget build(BuildContext context) { + assert(debugCheckHasMaterialLocalizations(context)); final MaterialLocalizations localizations = MaterialLocalizations.of(context); final ThemeData theme = Theme.of(context); final String title = widget.packageName; final String subtitle = localizations.licensesPackageDetailText(widget.licenseEntries.length); - const double pad = 24; - const EdgeInsets padding = EdgeInsets.only(left: pad, right: pad, bottom: pad); + final double pad = _getGutterSize(context); + final EdgeInsets padding = EdgeInsets.only(left: pad, right: pad, bottom: pad); final List listWidgets = [ ..._licenses, if (!_loaded) @@ -274,9 +293,11 @@ class _PackageLicensePageState extends State<_PackageLicensePage> { page = Scaffold( appBar: AppBar( title: _PackageLicensePageTitle( - title, - subtitle, - theme.primaryTextTheme, + title: title, + subtitle: subtitle, + theme: theme.useMaterial3 ? theme.textTheme : theme.primaryTextTheme, + titleTextStyle: theme.appBarTheme.titleTextStyle, + foregroundColor: theme.appBarTheme.foregroundColor, ), ), body: Center( @@ -292,7 +313,11 @@ class _PackageLicensePageState extends State<_PackageLicensePage> { // A Scrollbar is built-in below. behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), child: Scrollbar( - child: ListView(padding: padding, children: listWidgets), + child: ListView( + primary: true, + padding: padding, + children: listWidgets, + ), ), ), ), @@ -308,7 +333,12 @@ class _PackageLicensePageState extends State<_PackageLicensePage> { automaticallyImplyLeading: false, pinned: true, backgroundColor: theme.cardColor, - title: _PackageLicensePageTitle(title, subtitle, theme.textTheme), + title: _PackageLicensePageTitle( + title: title, + subtitle: subtitle, + theme: theme.textTheme, + titleTextStyle: theme.textTheme.titleLarge, + ), ), SliverPadding( padding: padding, @@ -334,27 +364,36 @@ class _PackageLicensePageState extends State<_PackageLicensePage> { } class _PackageLicensePageTitle extends StatelessWidget { - const _PackageLicensePageTitle( - this.title, - this.subtitle, - this.theme, - ); + const _PackageLicensePageTitle({ + required this.title, + required this.subtitle, + required this.theme, + this.titleTextStyle, + this.foregroundColor, + }); final String title; final String subtitle; final TextTheme theme; + final TextStyle? titleTextStyle; + final Color? foregroundColor; @override Widget build(BuildContext context) { - final Color? color = Theme.of(context).appBarTheme.foregroundColor; - + final TextStyle? effectiveTitleTextStyle = titleTextStyle ?? theme.titleLarge; return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: theme.titleLarge?.copyWith(color: color)), - Text(subtitle, style: theme.titleSmall?.copyWith(color: color)), + Text(title, style: effectiveTitleTextStyle?.copyWith(color: foregroundColor)), + Text(subtitle, style: theme.titleSmall?.copyWith(color: foregroundColor)), ], ); } } + +const int _materialGutterThreshold = 720; +const double _wideGutterSize = 24.0; +const double _narrowGutterSize = 12.0; + +double _getGutterSize(BuildContext context) => MediaQuery.sizeOf(context).width >= _materialGutterThreshold ? _wideGutterSize : _narrowGutterSize; diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 377e8b415..56129a592 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:math'; -import 'dart:ui'; import 'package:aves/app_flavor.dart'; import 'package:aves/app_mode.dart'; @@ -46,6 +45,7 @@ import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_localization_nn/flutter_localization_nn.dart'; import 'package:material_color_utilities/material_color_utilities.dart'; import 'package:overlay_support/overlay_support.dart'; import 'package:provider/provider.dart'; @@ -55,10 +55,22 @@ import 'package:url_launcher/url_launcher.dart' as ul; class AvesApp extends StatefulWidget { final AppFlavor flavor; + final Map? debugIntentData; // temporary exclude locales not ready yet for prime time // `ckb`: add `flutter_ckb_localization` and necessary app localization delegates when ready - static final _unsupportedLocales = {'ar', 'ckb', 'fa', 'gl', 'he', 'hi', 'nn', 'or', 'sk', 'th'}.map(Locale.new).toSet(); + static final _unsupportedLocales = { + 'ar', // Arabic + 'ckb', // Kurdish (Central) + 'fa', // Persian + 'gl', // Galician + 'he', // Hebrew + 'hi', // Hindi + 'ml', // Malayalam + 'or', // Odia + 'sk', // Slovak + 'th', // Thai + }.map(Locale.new).toSet(); static final List supportedLocales = AppLocalizations.supportedLocales.where((v) => !_unsupportedLocales.contains(v)).toList(); static final ValueNotifier cutoutInsetsNotifier = ValueNotifier(EdgeInsets.zero); @@ -74,6 +86,7 @@ class AvesApp extends StatefulWidget { const AvesApp({ super.key, required this.flavor, + this.debugIntentData, }); @override @@ -168,8 +181,6 @@ class _AvesAppState extends State with WidgetsBindingObserver { super.initState(); EquatableConfig.stringify = true; _appSetup = _setup(); - // remember screen size to use it later, when `context` and `window` are no longer reliable - _screenSize = _getScreenSize(); _shouldUseBoldFontLoader = AccessibilityService.shouldUseBoldFont(); _dynamicColorPaletteLoader = DynamicColorPlugin.getCorePalette(); _subscriptions.add(_mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChanged(event as String?))); @@ -194,6 +205,9 @@ class _AvesAppState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { + // remember screen size to use it later, when `context` and `window` are no longer reliable + _screenSize ??= _getScreenSize(context); + // place the settings provider above `MaterialApp` // so it can be used during navigation transitions return MultiProvider( @@ -215,7 +229,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { AvesApp.showSystemUI(); } final home = initialized - ? _getFirstPage() + ? _getFirstPage(intentData: widget.debugIntentData) : AvesScaffold( body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(), ); @@ -254,38 +268,34 @@ class _AvesAppState extends State with WidgetsBindingObserver { // KEYCODE_ENTER, KEYCODE_BUTTON_A, KEYCODE_NUMPAD_ENTER LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(), }, - child: MediaQuery.fromWindow( - child: Builder( - builder: (context) { - return MediaQuery( - data: MediaQuery.of(context).copyWith( - // disable accessible navigation, as it impacts snack bar action timer - // for all users of apps registered as accessibility services, - // even though they are not for accessibility purposes (like TalkBack is) - accessibleNavigation: false, - ), - child: MaterialApp( - navigatorKey: _navigatorKey, - home: home, - navigatorObservers: _navigatorObservers, - builder: (context, child) => _decorateAppChild( - context: context, - initialized: initialized, - child: child, - ), - onGenerateTitle: (context) => context.l10n.appName, - theme: lightTheme, - darkTheme: darkTheme, - themeMode: themeBrightness.appThemeMode, - locale: settingsLocale, - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AvesApp.supportedLocales, - // TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906 - scrollBehavior: StretchMaterialScrollBehavior(), - useInheritedMediaQuery: true, - ), - ); - }, + child: MediaQuery( + data: MediaQuery.of(context).copyWith( + // disable accessible navigation, as it impacts snack bar action timer + // for all users of apps registered as accessibility services, + // even though they are not for accessibility purposes (like TalkBack is) + accessibleNavigation: false, + ), + child: MaterialApp( + navigatorKey: _navigatorKey, + home: home, + navigatorObservers: _navigatorObservers, + builder: (context, child) => _decorateAppChild( + context: context, + initialized: initialized, + child: child, + ), + onGenerateTitle: (context) => context.l10n.appName, + theme: lightTheme, + darkTheme: darkTheme, + themeMode: themeBrightness.appThemeMode, + locale: settingsLocale, + localizationsDelegates: const [ + ...AppLocalizations.localizationsDelegates, + ...LocalizationsNn.delegates, + ], + supportedLocales: AvesApp.supportedLocales, + // TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906 + scrollBehavior: StretchMaterialScrollBehavior(), ), ), ); @@ -378,7 +388,6 @@ class _AvesAppState extends State with WidgetsBindingObserver { case AppMode.pickSingleMediaExternal: case AppMode.pickMultipleMediaExternal: _saveTopEntries(); - break; case AppMode.pickCollectionFiltersExternal: case AppMode.pickMediaInternal: case AppMode.pickFilterInternal: @@ -386,12 +395,11 @@ class _AvesAppState extends State with WidgetsBindingObserver { case AppMode.setWallpaper: case AppMode.slideshow: case AppMode.view: + case AppMode.edit: break; } - break; case AppLifecycleState.resumed: RecentlyAddedFilter.updateNow(); - break; case AppLifecycleState.paused: case AppLifecycleState.detached: break; @@ -409,9 +417,10 @@ class _AvesAppState extends State with WidgetsBindingObserver { Widget _getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage(); - Size? _getScreenSize() { - final physicalSize = window.physicalSize; - final ratio = window.devicePixelRatio; + Size? _getScreenSize(BuildContext context) { + final view = View.of(context); + final physicalSize = view.physicalSize; + final ratio = view.devicePixelRatio; return physicalSize > Size.zero && ratio > 0 ? physicalSize / ratio : null; } @@ -419,7 +428,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { void _saveTopEntries() { if (!settings.initialized) return; - final screenSize = _screenSize ?? _getScreenSize(); + final screenSize = _screenSize; if (screenSize == null) return; var tileExtent = settings.getTileExtent(CollectionPage.routeName); @@ -513,10 +522,8 @@ class _AvesAppState extends State with WidgetsBindingObserver { case MaxBrightness.never: case MaxBrightness.viewerOnly: ScreenBrightness().resetScreenBrightness(); - break; case MaxBrightness.always: ScreenBrightness().setScreenBrightness(1); - break; } } @@ -574,7 +581,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { : 'debug', 'has_mobile_services': mobileServices.isServiceAvailable, 'is_television': device.isTelevision, - 'locales': WidgetsBinding.instance.window.locales.join(', '), + 'locales': WidgetsBinding.instance.platformDispatcher.locales.join(', '), 'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})', }); await reportService.log('Launch'); diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index f3db7ad22..63a181814 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -224,7 +224,7 @@ class _CollectionAppBarState extends State with SingleTickerPr } double get appBarContentHeight { - final textScaleFactor = context.read().textScaleFactor; + final textScaleFactor = MediaQuery.textScaleFactorOf(context); double height = kToolbarHeight * textScaleFactor; if (settings.useTvLayout) { height += CaptionedButton.getTelevisionButtonHeight(context); @@ -511,16 +511,13 @@ class _CollectionAppBarState extends State with SingleTickerPr queryEnabled: context.read().enabled, isMenuItem: true, ); - break; case EntrySetAction.toggleFavourite: child = FavouriteToggler( entries: _getExpandedSelectedItems(selection), isMenuItem: true, ); - break; default: child = MenuRow(text: action.getText(context), icon: action.getIcon()); - break; } return PopupMenuItem( key: _getActionKey(action), @@ -598,7 +595,7 @@ class _CollectionAppBarState extends State with SingleTickerPr void _onQueryFocusRequest() => _queryBarFocusNode.requestFocus(); void _updateStatusBarHeight() { - _statusBarHeight = context.read().padding.top; + _statusBarHeight = MediaQuery.paddingOf(context).top; _updateAppBarHeight(); } @@ -611,16 +608,12 @@ class _CollectionAppBarState extends State with SingleTickerPr // general case EntrySetAction.configureView: await _configureView(); - break; case EntrySetAction.select: context.read>().select(); - break; case EntrySetAction.selectAll: context.read>().addToSelection(collection.sortedEntries); - break; case EntrySetAction.selectNone: context.read>().clearSelection(); - break; // browsing case EntrySetAction.searchCollection: case EntrySetAction.toggleTitleSearch: @@ -650,7 +643,6 @@ class _CollectionAppBarState extends State with SingleTickerPr case EntrySetAction.editTags: case EntrySetAction.removeMetadata: _actionDelegate.onActionSelected(context, action); - break; } } diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 23d4c148a..059bd0a63 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -46,7 +46,6 @@ import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:intl/intl.dart'; @@ -374,6 +373,8 @@ class _CollectionScaler extends StatelessWidget { final tileSpacing = metrics.item1; final horizontalPadding = metrics.item2; final brightness = Theme.of(context).brightness; + final borderColor = DecoratedThumbnail.borderColor; + final borderWidth = DecoratedThumbnail.borderWidth(context); return GridScaleGestureDetector( scrollableKey: scrollableKey, tileLayout: tileLayout, @@ -385,9 +386,9 @@ class _CollectionScaler extends StatelessWidget { tileSize: tileSize, spacing: tileSpacing, horizontalPadding: horizontalPadding, - borderWidth: DecoratedThumbnail.borderWidth, + borderWidth: borderWidth, borderRadius: Radius.zero, - color: DecoratedThumbnail.borderColor, + color: borderColor, textDirection: Directionality.of(context), ), child: child, @@ -404,8 +405,8 @@ class _CollectionScaler extends StatelessWidget { decoration: BoxDecoration( color: ThumbnailImage.computeLoadingBackgroundColor(index * 10, brightness).withOpacity(.9), border: Border.all( - color: DecoratedThumbnail.borderColor, - width: DecoratedThumbnail.borderWidth, + color: borderColor, + width: borderWidth, ), ), ), @@ -489,7 +490,6 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge } }); } - break; } } @@ -573,7 +573,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : SloppyScrollPhysics( - gestureSettings: context.select((mq) => mq.gestureSettings), + gestureSettings: MediaQuery.gestureSettingsOf(context), parent: const AlwaysScrollableScrollPhysics(), ), cacheExtent: context.select((controller) => controller.effectiveExtentMax), @@ -677,7 +677,6 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge switch (collection.sectionFactor) { case EntryGroupFactor.album: addAlbums(collection, sectionLayouts, crumbs); - break; case EntryGroupFactor.month: case EntryGroupFactor.day: final firstKey = sectionLayouts.first.sectionKey; @@ -701,14 +700,11 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge }); } } - break; case EntryGroupFactor.none: break; } - break; case EntrySortFactor.name: addAlbums(collection, sectionLayouts, crumbs); - break; case EntrySortFactor.rating: case EntrySortFactor.size: break; diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 0c3a1ac0e..93bc20b49 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -208,6 +208,7 @@ class _CollectionPageState extends State { case AppMode.setWallpaper: case AppMode.slideshow: case AppMode.view: + case AppMode.edit: return null; } } diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 908ad0861..2eb7a9bdc 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -173,79 +173,55 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware // browsing case EntrySetAction.searchCollection: _goToSearch(context); - break; case EntrySetAction.toggleTitleSearch: context.read().toggle(); - break; case EntrySetAction.addShortcut: _addShortcut(context); - break; // browsing or selecting case EntrySetAction.map: _goToMap(context); - break; case EntrySetAction.slideshow: _goToSlideshow(context); - break; case EntrySetAction.stats: _goToStats(context); - break; case EntrySetAction.rescan: _rescan(context); - break; // selecting case EntrySetAction.share: _share(context); - break; case EntrySetAction.delete: case EntrySetAction.emptyBin: _delete(context); - break; case EntrySetAction.restore: _move(context, moveType: MoveType.fromBin); - break; case EntrySetAction.copy: _move(context, moveType: MoveType.copy); - break; case EntrySetAction.move: _move(context, moveType: MoveType.move); - break; case EntrySetAction.rename: _rename(context); - break; case EntrySetAction.convert: _convert(context); - break; case EntrySetAction.toggleFavourite: _toggleFavourite(context); - break; case EntrySetAction.rotateCCW: _rotate(context, clockwise: false); - break; case EntrySetAction.rotateCW: _rotate(context, clockwise: true); - break; case EntrySetAction.flip: _flip(context); - break; case EntrySetAction.editDate: editDate(context); - break; case EntrySetAction.editLocation: _editLocation(context); - break; case EntrySetAction.editTitleDescription: _editTitleDescription(context); - break; case EntrySetAction.editRating: _editRating(context); - break; case EntrySetAction.editTags: _editTags(context); - break; case EntrySetAction.removeMetadata: _removeMetadata(context); - break; } } diff --git a/lib/widgets/collection/grid/headers/any.dart b/lib/widgets/collection/grid/headers/any.dart index c99c0c49d..556380fcc 100644 --- a/lib/widgets/collection/grid/headers/any.dart +++ b/lib/widgets/collection/grid/headers/any.dart @@ -57,7 +57,6 @@ class CollectionSectionHeader extends StatelessWidget { case EntryGroupFactor.none: break; } - break; case EntrySortFactor.name: return _buildAlbumHeader(context); case EntrySortFactor.rating: diff --git a/lib/widgets/collection/grid/list_details.dart b/lib/widgets/collection/grid/list_details.dart index 4d680b81d..1175dc0bd 100644 --- a/lib/widgets/collection/grid/list_details.dart +++ b/lib/widgets/collection/grid/list_details.dart @@ -78,7 +78,7 @@ class EntryListDetails extends StatelessWidget { Widget _buildDateRow(BuildContext context, TextStyle style) { final locale = context.l10n.localeName; - final use24hour = context.select((v) => v.alwaysUse24HourFormat); + final use24hour = MediaQuery.alwaysUse24HourFormatOf(context); final date = entry.bestDate; final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable; diff --git a/lib/widgets/collection/grid/tile.dart b/lib/widgets/collection/grid/tile.dart index da346a880..2a8cc56fc 100644 --- a/lib/widgets/collection/grid/tile.dart +++ b/lib/widgets/collection/grid/tile.dart @@ -41,23 +41,20 @@ class InteractiveTile extends StatelessWidget { } else { OpenViewerNotification(entry).dispatch(context); } - break; case AppMode.pickSingleMediaExternal: IntentService.submitPickedItems([entry.uri]); - break; case AppMode.pickMultipleMediaExternal: final selection = context.read>(); selection.toggleSelection(entry); - break; case AppMode.pickMediaInternal: Navigator.maybeOf(context)?.pop(entry); - break; case AppMode.pickCollectionFiltersExternal: case AppMode.pickFilterInternal: case AppMode.screenSaver: case AppMode.setWallpaper: case AppMode.slideshow: case AppMode.view: + case AppMode.edit: break; } }, diff --git a/lib/widgets/collection/query_bar.dart b/lib/widgets/collection/query_bar.dart index 005c7ec02..70a7a43e1 100644 --- a/lib/widgets/collection/query_bar.dart +++ b/lib/widgets/collection/query_bar.dart @@ -52,7 +52,7 @@ class _EntryQueryBarState extends State { @override Widget build(BuildContext context) { - final textScaleFactor = context.select((mq) => mq.textScaleFactor); + final textScaleFactor = MediaQuery.textScaleFactorOf(context); return Container( height: EntryQueryBar.getPreferredHeight(textScaleFactor), alignment: Alignment.topCenter, diff --git a/lib/widgets/common/action_controls/quick_choosers/common/menu.dart b/lib/widgets/common/action_controls/quick_choosers/common/menu.dart index 5beae8e82..aff175503 100644 --- a/lib/widgets/common/action_controls/quick_choosers/common/menu.dart +++ b/lib/widgets/common/action_controls/quick_choosers/common/menu.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:ui'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/quick_chooser.dart'; @@ -51,10 +50,15 @@ class _MenuQuickChooserState extends State> { @override void initState() { super.initState(); - _selectedRowRect.value = Rect.fromLTWH(0, window.physicalSize.height * (reversed ? 1 : -1), 0, 0); _registerWidget(widget); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _selectedRowRect.value = Rect.fromLTWH(0, MediaQuery.sizeOf(context).height * (reversed ? 1 : -1), 0, 0); + } + @override void didUpdateWidget(covariant MenuQuickChooser oldWidget) { super.didUpdateWidget(oldWidget); diff --git a/lib/widgets/common/action_controls/quick_choosers/common/route_layout.dart b/lib/widgets/common/action_controls/quick_choosers/common/route_layout.dart index 07a2b4624..835f69107 100644 --- a/lib/widgets/common/action_controls/quick_choosers/common/route_layout.dart +++ b/lib/widgets/common/action_controls/quick_choosers/common/route_layout.dart @@ -33,10 +33,8 @@ class QuickChooserRouteLayout extends SingleChildLayoutDelegate { switch (menuPosition) { case PopupMenuPosition.over: y = triggerRect.top - childSize.height; - break; case PopupMenuPosition.under: y = size.height - triggerRect.bottom; - break; } double x = (triggerRect.left + (size.width - triggerRect.right) - childSize.width) / 2; final wantedPosition = Offset(x, y); diff --git a/lib/widgets/common/action_mixins/entry_storage.dart b/lib/widgets/common/action_mixins/entry_storage.dart index 24ecffa80..c41508ae1 100644 --- a/lib/widgets/common/action_mixins/entry_storage.dart +++ b/lib/widgets/common/action_mixins/entry_storage.dart @@ -309,17 +309,14 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { ..remove(destinationAlbum) ..insert(0, destinationAlbum); entriesByDestination[destinationAlbum] = entries; - break; case MoveType.toBin: entriesByDestination[AndroidFileUtils.trashDirPath] = entries; - break; case MoveType.fromBin: groupBy(entries, (e) => e.directory).forEach((originAlbum, dirEntries) { if (originAlbum != null) { entriesByDestination[originAlbum] = dirEntries.toSet(); } }); - break; } await doQuickMove( diff --git a/lib/widgets/common/action_mixins/overlay_snack_bar.dart b/lib/widgets/common/action_mixins/overlay_snack_bar.dart index 778654cca..1c212f712 100644 --- a/lib/widgets/common/action_mixins/overlay_snack_bar.dart +++ b/lib/widgets/common/action_mixins/overlay_snack_bar.dart @@ -8,66 +8,82 @@ import 'package:flutter/material.dart'; // This overlay entry is not below a `Scaffold` (which is expected by `SnackBar` // and `SnackBarAction`), and is not dismissed the same way. // This adaptation assumes the `SnackBarBehavior.floating` behavior and no animation. -class OverlaySnackBar extends StatelessWidget { +class OverlaySnackBar extends StatefulWidget { final Widget content; final Widget? action; final DismissDirection dismissDirection; final VoidCallback onDismiss; + final Clip clipBehavior; const OverlaySnackBar({ super.key, required this.content, - required this.action, - required this.dismissDirection, + this.action, + this.dismissDirection = DismissDirection.down, + this.clipBehavior = Clip.hardEdge, required this.onDismiss, }); + @override + State createState() => _OverlaySnackBarState(); +} + +class _OverlaySnackBarState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - final snackBarTheme = theme.snackBarTheme; - final isThemeDark = theme.brightness == Brightness.dark; - final buttonColor = isThemeDark ? colorScheme.primary : colorScheme.secondary; + assert(debugCheckHasMediaQuery(context)); + final ThemeData theme = Theme.of(context); + final ColorScheme colorScheme = theme.colorScheme; + final SnackBarThemeData snackBarTheme = theme.snackBarTheme; + final bool isThemeDark = theme.brightness == Brightness.dark; + final Color buttonColor = isThemeDark ? colorScheme.primary : colorScheme.secondary; + final SnackBarThemeData defaults = theme.useMaterial3 ? _SnackbarDefaultsM3(context) : _SnackbarDefaultsM2(context); - final brightness = isThemeDark ? Brightness.light : Brightness.dark; - final themeBackgroundColor = isThemeDark ? colorScheme.onSurface : Color.alphaBlend(colorScheme.onSurface.withOpacity(0.80), colorScheme.surface); - final inverseTheme = theme.copyWith( - colorScheme: ColorScheme( - primary: colorScheme.onPrimary, - secondary: buttonColor, - surface: colorScheme.onSurface, - background: themeBackgroundColor, - error: colorScheme.onError, - onPrimary: colorScheme.primary, - onSecondary: colorScheme.secondary, - onSurface: colorScheme.surface, - onBackground: colorScheme.background, - onError: colorScheme.error, - brightness: brightness, - ), - ); + // SnackBar uses a theme that is the opposite brightness from + // the surrounding theme. + final Brightness brightness = isThemeDark ? Brightness.light : Brightness.dark; - final contentTextStyle = snackBarTheme.contentTextStyle ?? ThemeData(brightness: brightness).textTheme.titleMedium; + // Invert the theme values for Material 2. Material 3 values are tokenized to pre-inverted values. + final ThemeData effectiveTheme = theme.useMaterial3 + ? theme + : theme.copyWith( + colorScheme: ColorScheme( + primary: colorScheme.onPrimary, + secondary: buttonColor, + surface: colorScheme.onSurface, + background: defaults.backgroundColor!, + error: colorScheme.onError, + onPrimary: colorScheme.primary, + onSecondary: colorScheme.secondary, + onSurface: colorScheme.surface, + onBackground: colorScheme.background, + onError: colorScheme.error, + brightness: brightness, + ), + ); + + final TextStyle? contentTextStyle = snackBarTheme.contentTextStyle ?? defaults.contentTextStyle; final horizontalPadding = FeedbackMixin.snackBarHorizontalPadding(snackBarTheme); - final padding = EdgeInsetsDirectional.only(start: horizontalPadding, end: action != null ? 0 : horizontalPadding); + final padding = EdgeInsetsDirectional.only(start: horizontalPadding, end: widget.action != null ? 0 : horizontalPadding); const singleLineVerticalPadding = 14.0; + final EdgeInsets margin = snackBarTheme.insetPadding ?? defaults.insetPadding!; + Widget snackBar = Padding( padding: padding, child: Row( children: [ Expanded( child: Container( - padding: action != null ? null : const EdgeInsets.symmetric(vertical: singleLineVerticalPadding), + padding: widget.action != null ? null : const EdgeInsets.symmetric(vertical: singleLineVerticalPadding), child: DefaultTextStyle( style: contentTextStyle!, - child: content, + child: widget.content, ), ), ), - if (action != null) + if (widget.action != null) TextButtonTheme( data: TextButtonThemeData( style: TextButton.styleFrom( @@ -75,36 +91,28 @@ class OverlaySnackBar extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: horizontalPadding), ), ), - child: action!, + child: widget.action!, ), ], ), ); - final elevation = snackBarTheme.elevation ?? 6.0; - final backgroundColor = snackBarTheme.backgroundColor ?? inverseTheme.colorScheme.background; - final shape = snackBarTheme.shape ?? const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))); + final double elevation = snackBarTheme.elevation ?? defaults.elevation!; + final Color backgroundColor = snackBarTheme.backgroundColor ?? defaults.backgroundColor!; + final ShapeBorder? shape = snackBarTheme.shape ?? defaults.shape; snackBar = Material( shape: shape, elevation: elevation, color: backgroundColor, child: Theme( - data: inverseTheme, + data: effectiveTheme, child: snackBar, ), ); - const topMargin = 5.0; - const bottomMargin = 10.0; - const horizontalMargin = 15.0; snackBar = Padding( - padding: const EdgeInsets.fromLTRB( - horizontalMargin, - topMargin, - horizontalMargin, - bottomMargin, - ), + padding: margin, child: snackBar, ); @@ -117,16 +125,138 @@ class OverlaySnackBar extends StatelessWidget { snackBar = Semantics( container: true, liveRegion: true, - onDismiss: onDismiss, + onDismiss: widget.onDismiss, child: Dismissible( key: const Key('dismissible'), - direction: dismissDirection, + direction: widget.dismissDirection, resizeDuration: null, - onDismissed: (direction) => onDismiss(), + onDismissed: (direction) => widget.onDismiss(), child: snackBar, ), ); - return snackBar; + final Widget snackBarTransition = snackBar; + + return Hero( + tag: '', + transitionOnUserGestures: true, + child: ClipRect( + clipBehavior: widget.clipBehavior, + child: snackBarTransition, + ), + ); } } + +// Hand coded defaults based on Material Design 2. +class _SnackbarDefaultsM2 extends SnackBarThemeData { + _SnackbarDefaultsM2(BuildContext context) + : _theme = Theme.of(context), + _colors = Theme.of(context).colorScheme, + super(elevation: 6.0); + + late final ThemeData _theme; + late final ColorScheme _colors; + + @override + Color get backgroundColor => _theme.brightness == Brightness.light ? Color.alphaBlend(_colors.onSurface.withOpacity(0.80), _colors.surface) : _colors.onSurface; + + @override + TextStyle? get contentTextStyle => ThemeData(brightness: _theme.brightness == Brightness.light ? Brightness.dark : Brightness.light).textTheme.titleMedium; + + @override + SnackBarBehavior get behavior => SnackBarBehavior.fixed; + + @override + Color get actionTextColor => _colors.secondary; + + @override + Color get disabledActionTextColor => _colors.onSurface.withOpacity(_theme.brightness == Brightness.light ? 0.38 : 0.3); + + @override + ShapeBorder get shape => const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(4.0), + ), + ); + + @override + EdgeInsets get insetPadding => const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0); + + @override + bool get showCloseIcon => false; + + @override + Color get closeIconColor => _colors.onSurface; + + @override + double get actionOverflowThreshold => 0.25; +} + +// BEGIN GENERATED TOKEN PROPERTIES - Snackbar + +// Do not edit by hand. The code between the "BEGIN GENERATED" and +// "END GENERATED" comments are generated from data in the Material +// Design token database by the script: +// dev/tools/gen_defaults/bin/gen_defaults.dart. + +// Token database version: v0_162 + +class _SnackbarDefaultsM3 extends SnackBarThemeData { + _SnackbarDefaultsM3(this.context); + + final BuildContext context; + late final ThemeData _theme = Theme.of(context); + late final ColorScheme _colors = _theme.colorScheme; + + @override + Color get backgroundColor => _colors.inverseSurface; + + @override + Color get actionTextColor => MaterialStateColor.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return _colors.inversePrimary; + } + if (states.contains(MaterialState.pressed)) { + return _colors.inversePrimary; + } + if (states.contains(MaterialState.hovered)) { + return _colors.inversePrimary; + } + if (states.contains(MaterialState.focused)) { + return _colors.inversePrimary; + } + return _colors.inversePrimary; + }); + + @override + Color get disabledActionTextColor => _colors.inversePrimary; + + @override + TextStyle get contentTextStyle => Theme.of(context).textTheme.bodyMedium!.copyWith( + color: _colors.onInverseSurface, + ); + + @override + double get elevation => 6.0; + + @override + ShapeBorder get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))); + + @override + SnackBarBehavior get behavior => SnackBarBehavior.fixed; + + @override + EdgeInsets get insetPadding => const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 10.0); + + @override + bool get showCloseIcon => false; + + @override + Color? get closeIconColor => _colors.onInverseSurface; + + @override + double get actionOverflowThreshold => 0.25; +} + +// END GENERATED TOKEN PROPERTIES - Snackbar diff --git a/lib/widgets/common/action_mixins/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart index f8998d308..10723ce6f 100644 --- a/lib/widgets/common/action_mixins/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -35,7 +35,6 @@ mixin SizeAwareMixin { case MoveType.copy: case MoveType.export: needed = selection.fold(0, sumSize); - break; case MoveType.move: case MoveType.toBin: case MoveType.fromBin: @@ -46,7 +45,6 @@ mixin SizeAwareMixin { // and we need at least as much space as the largest entry because individual entries are copied then deleted final largestSingle = selection.fold(0, (largest, entry) => max(largest, entry.sizeBytes ?? 0)); needed = max(fromOtherVolumes, largestSingle); - break; } final hasEnoughSpace = needed < free; diff --git a/lib/widgets/common/action_mixins/vault_aware.dart b/lib/widgets/common/action_mixins/vault_aware.dart index 137591878..d900a3382 100644 --- a/lib/widgets/common/action_mixins/vault_aware.dart +++ b/lib/widgets/common/action_mixins/vault_aware.dart @@ -36,7 +36,6 @@ mixin VaultAwareMixin on FeedbackMixin { await reportService.recordError(e, stack); } } - break; case VaultLockType.pattern: final pattern = await showDialog( context: context, @@ -46,7 +45,6 @@ mixin VaultAwareMixin on FeedbackMixin { if (pattern != null) { confirmed = pattern == await securityService.readValue(details.passKey); } - break; case VaultLockType.pin: final pin = await showDialog( context: context, @@ -56,7 +54,6 @@ mixin VaultAwareMixin on FeedbackMixin { if (pin != null) { confirmed = pin == await securityService.readValue(details.passKey); } - break; case VaultLockType.password: final password = await showDialog( context: context, @@ -66,7 +63,6 @@ mixin VaultAwareMixin on FeedbackMixin { if (password != null) { confirmed = password == await securityService.readValue(details.passKey); } - break; } if (confirmed == null || !confirmed) return false; @@ -120,7 +116,6 @@ mixin VaultAwareMixin on FeedbackMixin { await reportService.recordError(e, stack); } } - break; case VaultLockType.pattern: final pattern = await showDialog( context: context, @@ -130,7 +125,6 @@ mixin VaultAwareMixin on FeedbackMixin { if (pattern != null) { return await securityService.writeValue(details.passKey, pattern); } - break; case VaultLockType.pin: final pin = await showDialog( context: context, @@ -140,7 +134,6 @@ mixin VaultAwareMixin on FeedbackMixin { if (pin != null) { return await securityService.writeValue(details.passKey, pin); } - break; case VaultLockType.password: final password = await showDialog( context: context, @@ -150,7 +143,6 @@ mixin VaultAwareMixin on FeedbackMixin { if (password != null) { return await securityService.writeValue(details.passKey, password); } - break; } return false; } diff --git a/lib/widgets/common/app_bar/app_bar_title.dart b/lib/widgets/common/app_bar/app_bar_title.dart index 19283c9d9..a0274db45 100644 --- a/lib/widgets/common/app_bar/app_bar_title.dart +++ b/lib/widgets/common/app_bar/app_bar_title.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class InteractiveAppBarTitle extends StatelessWidget { final GestureTapCallback? onTap; @@ -13,6 +12,7 @@ class InteractiveAppBarTitle extends StatelessWidget { @override Widget build(BuildContext context) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); return GestureDetector( onTap: onTap, // use a `Container` with a dummy color to make it expand @@ -20,7 +20,7 @@ class InteractiveAppBarTitle extends StatelessWidget { child: Container( alignment: AlignmentDirectional.centerStart, color: Colors.transparent, - height: kToolbarHeight * context.select((mq) => mq.textScaleFactor), + height: kToolbarHeight * textScaleFactor, child: child, ), ); diff --git a/lib/widgets/common/basic/font_size_icon_theme.dart b/lib/widgets/common/basic/font_size_icon_theme.dart index ea43d4449..7b143fcf8 100644 --- a/lib/widgets/common/basic/font_size_icon_theme.dart +++ b/lib/widgets/common/basic/font_size_icon_theme.dart @@ -11,10 +11,11 @@ class FontSizeIconTheme extends StatelessWidget { @override Widget build(BuildContext context) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); final iconTheme = IconTheme.of(context); return IconTheme( data: iconTheme.copyWith( - size: iconTheme.size! * MediaQuery.textScaleFactorOf(context), + size: iconTheme.size! * textScaleFactor, ), child: child, ); diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart index fa1a73d46..106756878 100644 --- a/lib/widgets/common/basic/insets.dart +++ b/lib/widgets/common/basic/insets.dart @@ -22,7 +22,7 @@ class BottomGestureAreaProtector extends StatelessWidget { left: 0, right: 0, bottom: 0, - height: context.select((mq) => mq.systemGestureInsets.bottom), + height: MediaQuery.systemGestureInsetsOf(context).bottom, child: GestureDetector( // absorb vertical gestures only onVerticalDragDown: (details) {}, @@ -42,7 +42,7 @@ class TopGestureAreaProtector extends StatelessWidget { left: 0, top: 0, right: 0, - height: context.select((mq) => mq.systemGestureInsets.top), + height: MediaQuery.systemGestureInsetsOf(context).top, child: GestureDetector( // absorb vertical gestures only onVerticalDragDown: (details) {}, @@ -64,7 +64,7 @@ class SideGestureAreaProtector extends StatelessWidget { textDirection: TextDirection.ltr, children: [ SizedBox( - width: context.select((mq) => mq.systemGestureInsets.left), + width: MediaQuery.systemGestureInsetsOf(context).left, child: GestureDetector( // absorb horizontal gestures only onHorizontalDragDown: (details) {}, @@ -73,7 +73,7 @@ class SideGestureAreaProtector extends StatelessWidget { ), const Spacer(), SizedBox( - width: context.select((mq) => mq.systemGestureInsets.right), + width: MediaQuery.systemGestureInsetsOf(context).right, child: GestureDetector( // absorb horizontal gestures only onHorizontalDragDown: (details) {}, diff --git a/lib/widgets/common/basic/list_tiles/reselectable_radio.dart b/lib/widgets/common/basic/list_tiles/reselectable_radio.dart index b3d456627..f6a33aacf 100644 --- a/lib/widgets/common/basic/list_tiles/reselectable_radio.dart +++ b/lib/widgets/common/basic/list_tiles/reselectable_radio.dart @@ -54,11 +54,9 @@ class ReselectableRadioListTile extends StatelessWidget { case ListTileControlAffinity.platform: leading = control; trailing = secondary; - break; case ListTileControlAffinity.trailing: leading = secondary; trailing = control; - break; } return MergeSemantics( child: ListTileTheme.merge( diff --git a/lib/widgets/common/basic/markdown_container.dart b/lib/widgets/common/basic/markdown_container.dart index 14cd63158..62c054210 100644 --- a/lib/widgets/common/basic/markdown_container.dart +++ b/lib/widgets/common/basic/markdown_container.dart @@ -54,7 +54,7 @@ class MarkdownContainer extends StatelessWidget { margin: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: Theme.of(context).canvasColor, - border: Border.all(color: Theme.of(context).dividerColor, width: AvesBorder.curvedBorderWidth), + border: Border.all(color: Theme.of(context).dividerColor, width: AvesBorder.curvedBorderWidth(context)), borderRadius: const BorderRadius.all(Radius.circular(16)), ), constraints: BoxConstraints(maxWidth: useTvLayout ? double.infinity : mobileMaxWidth), diff --git a/lib/widgets/common/basic/multi_cross_fader.dart b/lib/widgets/common/basic/multi_cross_fader.dart index 17a07ad9b..df65a57af 100644 --- a/lib/widgets/common/basic/multi_cross_fader.dart +++ b/lib/widgets/common/basic/multi_cross_fader.dart @@ -4,6 +4,7 @@ class MultiCrossFader extends StatefulWidget { final Duration duration; final Curve fadeCurve, sizeCurve; final AlignmentGeometry alignment; + final AnimatedCrossFadeBuilder layoutBuilder; final Widget child; const MultiCrossFader({ @@ -12,6 +13,7 @@ class MultiCrossFader extends StatefulWidget { this.fadeCurve = Curves.linear, this.sizeCurve = Curves.linear, this.alignment = Alignment.topCenter, + this.layoutBuilder = AnimatedCrossFade.defaultLayoutBuilder, required this.child, }); @@ -53,6 +55,8 @@ class _MultiCrossFaderState extends State { alignment: widget.alignment, crossFadeState: _fadeState, duration: widget.duration, + reverseDuration: widget.duration, + layoutBuilder: widget.layoutBuilder, ); } } diff --git a/lib/widgets/common/basic/scaffold.dart b/lib/widgets/common/basic/scaffold.dart index e70c0fdda..81058a618 100644 --- a/lib/widgets/common/basic/scaffold.dart +++ b/lib/widgets/common/basic/scaffold.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class AvesScaffold extends StatelessWidget { final PreferredSizeWidget? appBar; @@ -26,7 +25,7 @@ class AvesScaffold extends StatelessWidget { @override Widget build(BuildContext context) { // prevent conflict between drawer drag gesture and Android navigation gestures - final drawerEnableOpenDragGesture = context.select((mq) => mq.systemGestureInsets.horizontal == 0); + final drawerEnableOpenDragGesture = MediaQuery.systemGestureInsetsOf(context).horizontal == 0; return Scaffold( appBar: appBar, diff --git a/lib/widgets/common/basic/text/outlined.dart b/lib/widgets/common/basic/text/outlined.dart index 22630b843..3ccf14529 100644 --- a/lib/widgets/common/basic/text/outlined.dart +++ b/lib/widgets/common/basic/text/outlined.dart @@ -31,44 +31,51 @@ class OutlinedText extends StatelessWidget { @override Widget build(BuildContext context) { // TODO TLAD [subtitles] fix background area for mixed alphabetic-ideographic text - // as of Flutter v2.2.2, the area computed for `backgroundColor` has inconsistent height - // in case of mixed alphabetic-ideographic text. The painted boxes depends on the script. + // as of Flutter v3.10.0, the area computed for `backgroundColor` has inconsistent height + // in case of mixed alphabetic-ideographic text. The painted boxes depend on the script. // Possible workarounds would be to use metrics from: // - `TextPainter.getBoxesForSelection` // - `Paragraph.getBoxesForRange` // and paint the background at the bottom of the `Stack` + final hasOutline = outlineWidth > 0; + + Widget? outline; + if (hasOutline) { + outline = Text.rich( + TextSpan( + children: textSpans.map(_toStrokeSpan).toList(), + ), + textAlign: textAlign, + softWrap: softWrap, + overflow: overflow, + maxLines: maxLines, + ); + if (outlineBlurSigma > 0) { + outline = ImageFiltered( + imageFilter: ImageFilter.blur( + sigmaX: outlineBlurSigma, + sigmaY: outlineBlurSigma, + ), + child: outline, + ); + } + } + + final fill = Text.rich( + TextSpan( + children: hasOutline ? textSpans.map(_toFillSpan).toList() : textSpans, + ), + textAlign: textAlign, + softWrap: softWrap, + overflow: overflow, + maxLines: maxLines, + ); + return Stack( children: [ - if (hasOutline) - ImageFiltered( - imageFilter: outlineBlurSigma > 0 - ? ImageFilter.blur( - sigmaX: outlineBlurSigma, - sigmaY: outlineBlurSigma, - ) - : ImageFilter.matrix( - Matrix4.identity().storage, - ), - child: Text.rich( - TextSpan( - children: textSpans.map(_toStrokeSpan).toList(), - ), - textAlign: textAlign, - softWrap: softWrap, - overflow: overflow, - maxLines: maxLines, - ), - ), - Text.rich( - TextSpan( - children: hasOutline ? textSpans.map(_toFillSpan).toList() : textSpans, - ), - textAlign: textAlign, - softWrap: softWrap, - overflow: overflow, - maxLines: maxLines, - ), + if (outline != null) outline, + fill, ], ); } @@ -89,6 +96,7 @@ class OutlinedText extends StatelessWidget { children: span.children, style: (span.style ?? const TextStyle()).copyWith( backgroundColor: Colors.transparent, + shadows: [], ), ); } diff --git a/lib/widgets/common/basic/wheel.dart b/lib/widgets/common/basic/wheel.dart index b6f190380..1682eb5cb 100644 --- a/lib/widgets/common/basic/wheel.dart +++ b/lib/widgets/common/basic/wheel.dart @@ -46,10 +46,11 @@ class _WheelSelectorState extends State> { @override Widget build(BuildContext context) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); const background = Colors.transparent; final foreground = DefaultTextStyle.of(context).style.color!; final transitionDuration = context.select((v) => v.formTransition); - final itemSize = Size.square(40 * context.select((mq) => mq.textScaleFactor)); + final itemSize = Size.square(40 * textScaleFactor); return FocusableActionDetector( shortcuts: const { @@ -138,10 +139,8 @@ class _WheelSelectorState extends State> { switch (intent.type) { case _ValueAdjustmentType.up: delta = -1; - break; case _ValueAdjustmentType.down: delta = 1; - break; } final targetItem = _controller.selectedItem + delta; final duration = context.read().formTransition; diff --git a/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart b/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart index 9149e0b63..ce2b4c682 100644 --- a/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart +++ b/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart @@ -26,20 +26,47 @@ enum _ScaleState { } class _PointerPanZoomData { - _PointerPanZoomData({required this.focalPoint, required this.scale, required this.rotation}); + _PointerPanZoomData.fromStartEvent(this.parent, PointerPanZoomStartEvent event) + : _position = event.position, + _pan = Offset.zero, + _scale = 1, + _rotation = 0; - Offset focalPoint; - double scale; - double rotation; + _PointerPanZoomData.fromUpdateEvent(this.parent, PointerPanZoomUpdateEvent event) + : _position = event.position, + _pan = event.pan, + _scale = event.scale, + _rotation = event.rotation; + + final EagerScaleGestureRecognizer parent; + final Offset _position; + final Offset _pan; + final double _scale; + final double _rotation; + + Offset get focalPoint { + if (parent.trackpadScrollCausesScale) { + return _position; + } + return _position + _pan; + } + + double get scale { + if (parent.trackpadScrollCausesScale) { + return _scale * math.exp((_pan.dx * parent.trackpadScrollToScaleFactor.dx) + (_pan.dy * parent.trackpadScrollToScaleFactor.dy)); + } + return _scale; + } + + double get rotation => _rotation; @override - String toString() => '_PointerPanZoomData(focalPoint: $focalPoint, scale: $scale, angle: $rotation)'; + String toString() => '_PointerPanZoomData(parent: $parent, _position: $_position, _pan: $_pan, _scale: $_scale, _rotation: $_rotation)'; } //////////////////////////////////////////////////////////////////////////////// bool _isFlingGesture(Velocity velocity) { - assert(velocity != null); final double speedSquared = velocity.pixelsPerSecond.distanceSquared; return speedSquared > kMinFlingVelocity * kMinFlingVelocity; } @@ -57,9 +84,7 @@ class _LineBetweenPointers { this.pointerStartId = 0, this.pointerEndLocation = Offset.zero, this.pointerEndId = 1, - }) : assert(pointerStartLocation != null && pointerEndLocation != null), - assert(pointerStartId != null && pointerEndId != null), - assert(pointerStartId != pointerEndId); + }) : assert(pointerStartId != pointerEndId); // The location and the id of the pointer that marks the start of the line. final Offset pointerStartLocation; @@ -74,23 +99,22 @@ class _LineBetweenPointers { /// /// [ScaleGestureRecognizer] tracks the pointers in contact with the screen and /// calculates their focal point, indicated scale, and rotation. When a focal -/// pointer is established, the recognizer calls [onStart]. As the focal point, -/// scale, rotation change, the recognizer calls [onUpdate]. When the pointers -/// are no longer in contact with the screen, the recognizer calls [onEnd]. +/// point is established, the recognizer calls [onStart]. As the focal point, +/// scale, and rotation change, the recognizer calls [onUpdate]. When the +/// pointers are no longer in contact with the screen, the recognizer calls +/// [onEnd]. class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { /// Create a gesture recognizer for interactions intended for scaling content. /// /// {@macro flutter.gestures.GestureRecognizer.supportedDevices} EagerScaleGestureRecognizer({ super.debugOwner, - @Deprecated( - 'Migrate to supportedDevices. ' - 'This feature was deprecated after v2.3.0-1.0.pre.', - ) - super.kind, super.supportedDevices, + super.allowedButtonsFilter, this.dragStartBehavior = DragStartBehavior.down, - }) : assert(dragStartBehavior != null); + this.trackpadScrollCausesScale = false, + this.trackpadScrollToScaleFactor = kDefaultTrackpadScrollToScaleFactor, + }); /// Determines what point is used as the starting point in all calculations /// involving this gesture. @@ -137,6 +161,26 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { Matrix4? _lastTransform; + /// {@template flutter.gestures.scale.trackpadScrollCausesScale} + /// Whether scrolling up/down on a trackpad should cause scaling instead of + /// panning. + /// + /// Defaults to false. + /// {@endtemplate} + bool trackpadScrollCausesScale; + + /// {@template flutter.gestures.scale.trackpadScrollToScaleFactor} + /// A factor to control the direction and magnitude of scale when converting + /// trackpad scrolling. + /// + /// Incoming trackpad pan offsets will be divided by this factor to get scale + /// values. Increasing this offset will reduce the amount of scaling caused by + /// a fixed amount of trackpad scrolling. + /// + /// Defaults to [kDefaultTrackpadScrollToScaleFactor]. + /// {@endtemplate} + Offset trackpadScrollToScaleFactor; + late Offset _initialFocalPoint; Offset? _currentFocalPoint; late double _initialSpan; @@ -151,6 +195,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { final Map _pointerLocations = {}; final List _pointerQueue = []; // A queue to sort pointers in order of entrance final Map _velocityTrackers = {}; + VelocityTracker? _scaleVelocityTracker; late Offset _delta; final Map _pointerPanZooms = {}; double _initialPanZoomScaleFactor = 1; @@ -271,15 +316,16 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { _lastTransform = event.transform; } else if (event is PointerPanZoomStartEvent) { assert(_pointerPanZooms[event.pointer] == null); - _pointerPanZooms[event.pointer] = _PointerPanZoomData(focalPoint: event.position, scale: 1, rotation: 0); + _pointerPanZooms[event.pointer] = _PointerPanZoomData.fromStartEvent(this, event); didChangeConfiguration = true; shouldStartIfAccepted = true; + _lastTransform = event.transform; } else if (event is PointerPanZoomUpdateEvent) { assert(_pointerPanZooms[event.pointer] != null); - if (!event.synthesized) { + if (!event.synthesized && !trackpadScrollCausesScale) { _velocityTrackers[event.pointer]!.addPosition(event.timeStamp, event.pan); } - _pointerPanZooms[event.pointer] = _PointerPanZoomData(focalPoint: event.position + event.pan, scale: event.scale, rotation: event.rotation); + _pointerPanZooms[event.pointer] = _PointerPanZoomData.fromUpdateEvent(this, event); _lastTransform = event.transform; shouldStartIfAccepted = true; } else if (event is PointerPanZoomEndEvent) { @@ -292,7 +338,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { _update(); if (!didChangeConfiguration || _reconfigure(event.pointer)) { - _advanceStateMachine(shouldStartIfAccepted, event.kind); + _advanceStateMachine(shouldStartIfAccepted, event); } stopTrackingIfPointerNoLongerDown(event); } @@ -403,18 +449,20 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) { velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity); } - invokeCallback('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerCount))); + invokeCallback('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: _pointerCount))); } else { - invokeCallback('onEnd', () => onEnd!(ScaleEndDetails(pointerCount: _pointerCount))); + invokeCallback('onEnd', () => onEnd!(ScaleEndDetails(scaleVelocity: _scaleVelocityTracker?.getVelocity().pixelsPerSecond.dx ?? -1, pointerCount: _pointerCount))); } } _state = _ScaleState.accepted; + _scaleVelocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch); // arbitrary PointerDeviceKind return false; } + _scaleVelocityTracker = VelocityTracker.withKind(PointerDeviceKind.touch); // arbitrary PointerDeviceKind return true; } - void _advanceStateMachine(bool shouldStartIfAccepted, PointerDeviceKind pointerDeviceKind) { + void _advanceStateMachine(bool shouldStartIfAccepted, PointerEvent event) { if (_state == _ScaleState.ready) { _state = _ScaleState.possible; } @@ -428,7 +476,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { if (_state == _ScaleState.possible) { final double spanDelta = (_currentSpan - _initialSpan).abs(); final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance; - if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind, gestureSettings) || math.max(_scaleFactor / _pointerScaleFactor, _pointerScaleFactor / _scaleFactor) > 1.05) { + if (spanDelta > computeScaleSlop(event.kind) || focalPointDelta > computePanSlop(event.kind, gestureSettings) || math.max(_scaleFactor / _pointerScaleFactor, _pointerScaleFactor / _scaleFactor) > 1.05) { resolve(GestureDisposition.accepted); } } else if (_state.index >= _ScaleState.accepted.index) { @@ -440,19 +488,22 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { _dispatchOnStartCallbackIfNeeded(); } - if (_state == _ScaleState.started && onUpdate != null) { - invokeCallback('onUpdate', () { - onUpdate!(ScaleUpdateDetails( - scale: _scaleFactor, - horizontalScale: _horizontalScaleFactor, - verticalScale: _verticalScaleFactor, - focalPoint: _currentFocalPoint!, - localFocalPoint: _localFocalPoint, - rotation: _computeRotationFactor(), - pointerCount: _pointerCount, - focalPointDelta: _delta, - )); - }); + if (_state == _ScaleState.started) { + _scaleVelocityTracker?.addPosition(event.timeStamp, Offset(_scaleFactor, 0)); + if (onUpdate != null) { + invokeCallback('onUpdate', () { + onUpdate!(ScaleUpdateDetails( + scale: _scaleFactor, + horizontalScale: _horizontalScaleFactor, + verticalScale: _verticalScaleFactor, + focalPoint: _currentFocalPoint!, + localFocalPoint: _localFocalPoint, + rotation: _computeRotationFactor(), + pointerCount: _pointerCount, + focalPointDelta: _delta, + )); + }); + } } } @@ -504,15 +555,12 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { switch (_state) { case _ScaleState.possible: resolve(GestureDisposition.rejected); - break; case _ScaleState.ready: assert(false); // We should have not seen a pointer yet - break; case _ScaleState.accepted: break; case _ScaleState.started: assert(false); // We should be in the accepted state when user is done - break; } _state = _ScaleState.ready; } diff --git a/lib/widgets/common/behaviour/intents.dart b/lib/widgets/common/behaviour/intents.dart index d58dc2e8f..98dcfd723 100644 --- a/lib/widgets/common/behaviour/intents.dart +++ b/lib/widgets/common/behaviour/intents.dart @@ -14,11 +14,9 @@ class ScrollControllerAction extends CallbackAction { case AxisDirection.up: case AxisDirection.left: factor = -1; - break; case AxisDirection.down: case AxisDirection.right: factor = 1; - break; } scrollController.animateTo( scrollController.offset + factor * 150, diff --git a/lib/widgets/common/behaviour/known_extent_scroll_physics.dart b/lib/widgets/common/behaviour/known_extent_scroll_physics.dart index 0dce20b84..57bb1b809 100644 --- a/lib/widgets/common/behaviour/known_extent_scroll_physics.dart +++ b/lib/widgets/common/behaviour/known_extent_scroll_physics.dart @@ -53,7 +53,7 @@ class KnownExtentScrollPhysics extends ScrollPhysics { // Scenario 3: // If there's no velocity and we're already at where we intend to land, // do nothing. - if (velocity.abs() < tolerance.velocity && (settlingPixels - metrics.pixels).abs() < tolerance.distance) { + if (velocity.abs() < toleranceFor(position).velocity && (settlingPixels - metrics.pixels).abs() < toleranceFor(position).distance) { return null; } @@ -66,7 +66,7 @@ class KnownExtentScrollPhysics extends ScrollPhysics { metrics.pixels, settlingPixels, velocity, - tolerance: tolerance, + tolerance: toleranceFor(position), ); } @@ -77,7 +77,7 @@ class KnownExtentScrollPhysics extends ScrollPhysics { metrics.pixels, settlingPixels, velocity, - tolerance.velocity * velocity.sign, + toleranceFor(position).velocity * velocity.sign, ); } } diff --git a/lib/widgets/common/behaviour/pop/tv_navigation.dart b/lib/widgets/common/behaviour/pop/tv_navigation.dart index 8af4081ff..e51367139 100644 --- a/lib/widgets/common/behaviour/pop/tv_navigation.dart +++ b/lib/widgets/common/behaviour/pop/tv_navigation.dart @@ -5,6 +5,7 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; +import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -33,25 +34,25 @@ class TvNavigationPopHandler { case HomePageSetting.collection: return context.read().filters.isEmpty; case HomePageSetting.albums: + case HomePageSetting.tags: return true; } } static Route _getHomeRoute() { - switch (settings.homePage) { + final homePage = settings.homePage; + Route buildRoute(WidgetBuilder builder) => MaterialPageRoute( + settings: RouteSettings(name: homePage.routeName), + builder: builder, + ); + + switch (homePage) { case HomePageSetting.collection: - return MaterialPageRoute( - settings: const RouteSettings(name: CollectionPage.routeName), - builder: (context) => CollectionPage( - source: context.read(), - filters: null, - ), - ); + return buildRoute((context) => CollectionPage(source: context.read(), filters: null)); case HomePageSetting.albums: - return MaterialPageRoute( - settings: const RouteSettings(name: AlbumListPage.routeName), - builder: (context) => const AlbumListPage(), - ); + return buildRoute((context) => const AlbumListPage()); + case HomePageSetting.tags: + return buildRoute((context) => const TagListPage()); } } } diff --git a/lib/widgets/common/behaviour/springy_scroll_physics.dart b/lib/widgets/common/behaviour/springy_scroll_physics.dart index 1b39c0540..51b887cde 100644 --- a/lib/widgets/common/behaviour/springy_scroll_physics.dart +++ b/lib/widgets/common/behaviour/springy_scroll_physics.dart @@ -16,5 +16,4 @@ class SpringyScrollPhysics extends ScrollPhysics { parent: buildParent(ancestor), ); } - } diff --git a/lib/widgets/common/extensions/geometry.dart b/lib/widgets/common/extensions/geometry.dart new file mode 100644 index 000000000..3979c4ab3 --- /dev/null +++ b/lib/widgets/common/extensions/geometry.dart @@ -0,0 +1,7 @@ +import 'dart:ui'; + +extension ExtraRect on Rect { + bool containsIncludingBottomRight(Offset offset, {double tolerance = 0}) { + return offset.dx >= left - tolerance && offset.dx <= right + tolerance && offset.dy >= top - tolerance && offset.dy <= bottom + tolerance; + } +} diff --git a/lib/widgets/common/fx/borders.dart b/lib/widgets/common/fx/borders.dart index b9dc02a0d..cc474869a 100644 --- a/lib/widgets/common/fx/borders.dart +++ b/lib/widgets/common/fx/borders.dart @@ -1,26 +1,22 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; class AvesBorder { static Color _borderColor(BuildContext context) => Theme.of(context).brightness == Brightness.dark ? Colors.white30 : Colors.black26; - // directly uses `devicePixelRatio` as it never changes, to avoid visiting ancestors via `MediaQuery` - // 1 device pixel for straight lines is fine - static double get straightBorderWidth => 1 / window.devicePixelRatio; + static double straightBorderWidth(BuildContext context) => 1 / View.of(context).devicePixelRatio; // 1 device pixel for curves is too thin - static double get curvedBorderWidth => window.devicePixelRatio > 2 ? 0.5 : 1.0; + static double curvedBorderWidth(BuildContext context) => View.of(context).devicePixelRatio > 2 ? 0.5 : 1.0; static BorderSide straightSide(BuildContext context, {double? width}) => BorderSide( color: _borderColor(context), - width: width ?? straightBorderWidth, + width: width ?? straightBorderWidth(context), ); static BorderSide curvedSide(BuildContext context, {double? width}) => BorderSide( color: _borderColor(context), - width: width ?? curvedBorderWidth, + width: width ?? curvedBorderWidth(context), ); static Border border(BuildContext context, {double? width}) => Border.fromBorderSide(curvedSide(context, width: width)); diff --git a/lib/widgets/common/fx/checkered_decoration.dart b/lib/widgets/common/fx/checkered_decoration.dart index ad2de6e62..b7fb249fd 100644 --- a/lib/widgets/common/fx/checkered_decoration.dart +++ b/lib/widgets/common/fx/checkered_decoration.dart @@ -15,7 +15,7 @@ class CheckeredPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final background = Rect.fromLTWH(0, 0, size.width, size.height); + final background = Offset.zero & size; canvas.drawRect(background, lightPaint); final dx = offset.dx % (checkSize * 2); diff --git a/lib/widgets/common/fx/dashed_path_painter.dart b/lib/widgets/common/fx/dashed_path_painter.dart new file mode 100644 index 000000000..5d0c36d56 --- /dev/null +++ b/lib/widgets/common/fx/dashed_path_painter.dart @@ -0,0 +1,142 @@ +import 'dart:ui' as ui; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +// from https://stackoverflow.com/a/71099304/786656 +class DashedPathPainter extends CustomPainter { + final Path originalPath; + final Color pathColor; + final double strokeWidth; + final double dashGapLength; + final double dashLength; + late DashedPathProperties _dashedPathProperties; + + DashedPathPainter({ + required this.originalPath, + required this.pathColor, + this.strokeWidth = 3.0, + this.dashGapLength = 5.0, + this.dashLength = 10.0, + }); + + @override + void paint(Canvas canvas, Size size) { + _dashedPathProperties = DashedPathProperties( + path: Path(), + dashLength: dashLength, + dashGapLength: dashGapLength, + ); + final dashedPath = _getDashedPath(originalPath, dashLength, dashGapLength); + canvas.drawPath( + dashedPath, + Paint() + ..style = PaintingStyle.stroke + ..color = pathColor + ..strokeWidth = strokeWidth, + ); + } + + @override + bool shouldRepaint(DashedPathPainter oldDelegate) => oldDelegate.originalPath != originalPath || oldDelegate.pathColor != pathColor || oldDelegate.strokeWidth != strokeWidth || oldDelegate.dashGapLength != dashGapLength || oldDelegate.dashLength != dashLength; + + Path _getDashedPath( + Path originalPath, + double dashLength, + double dashGapLength, + ) { + final metricsIterator = originalPath.computeMetrics().iterator; + while (metricsIterator.moveNext()) { + final metric = metricsIterator.current; + _dashedPathProperties.extractedPathLength = 0.0; + while (_dashedPathProperties.extractedPathLength < metric.length) { + if (_dashedPathProperties.addDashNext) { + _dashedPathProperties.addDash(metric, dashLength); + } else { + _dashedPathProperties.addDashGap(metric, dashGapLength); + } + } + } + return _dashedPathProperties.path; + } +} + +class DashedPathProperties { + double extractedPathLength; + Path path; + + final double _dashLength; + double _remainingDashLength; + double _remainingDashGapLength; + bool _previousWasDash; + + DashedPathProperties({ + required this.path, + required double dashLength, + required double dashGapLength, + }) : assert(dashLength > 0.0, 'dashLength must be > 0.0'), + assert(dashGapLength > 0.0, 'dashGapLength must be > 0.0'), + _dashLength = dashLength, + _remainingDashLength = dashLength, + _remainingDashGapLength = dashGapLength, + _previousWasDash = false, + extractedPathLength = 0.0; + + bool get addDashNext { + if (!_previousWasDash || _remainingDashLength != _dashLength) { + return true; + } + return false; + } + + void addDash(ui.PathMetric metric, double dashLength) { + // Calculate lengths (actual + available) + final end = _calculateLength(metric, _remainingDashLength); + final availableEnd = _calculateLength(metric, dashLength); + // Add path + final pathSegment = metric.extractPath(extractedPathLength, end); + path.addPath(pathSegment, Offset.zero); + // Update + final delta = _remainingDashLength - (end - extractedPathLength); + _remainingDashLength = _updateRemainingLength( + delta: delta, + end: end, + availableEnd: availableEnd, + initialLength: dashLength, + ); + extractedPathLength = end; + _previousWasDash = true; + } + + void addDashGap(ui.PathMetric metric, double dashGapLength) { + // Calculate lengths (actual + available) + final end = _calculateLength(metric, _remainingDashGapLength); + final availableEnd = _calculateLength(metric, dashGapLength); + // Move path's end point + ui.Tangent tangent = metric.getTangentForOffset(end)!; + path.moveTo(tangent.position.dx, tangent.position.dy); + // Update + final delta = end - extractedPathLength; + _remainingDashGapLength = _updateRemainingLength( + delta: delta, + end: end, + availableEnd: availableEnd, + initialLength: dashGapLength, + ); + extractedPathLength = end; + _previousWasDash = false; + } + + double _calculateLength(ui.PathMetric metric, double addedLength) { + return math.min(extractedPathLength + addedLength, metric.length); + } + + double _updateRemainingLength({ + required double delta, + required double end, + required double availableEnd, + required double initialLength, + }) { + return (delta > 0 && availableEnd == end) ? delta : initialLength; + } +} diff --git a/lib/widgets/common/fx/transition_image.dart b/lib/widgets/common/fx/transition_image.dart index 4ee1b8390..abd417506 100644 --- a/lib/widgets/common/fx/transition_image.dart +++ b/lib/widgets/common/fx/transition_image.dart @@ -2,6 +2,8 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/semantics.dart'; // adapted from Flutter `_ImageState` in `/widgets/image.dart` // and `DecorationImagePainter` in `/painting/decoration_image.dart` @@ -13,6 +15,11 @@ class TransitionImage extends StatefulWidget { final ImageProvider image; final ValueListenable animation; final BoxFit thumbnailFit, viewerFit; + final ImageFrameBuilder? frameBuilder; + final ImageLoadingBuilder? loadingBuilder; + final ImageErrorWidgetBuilder? errorBuilder; + final String? semanticLabel; + final bool excludeFromSemantics; final double? width, height; final bool gaplessPlayback = false; final Color? background; @@ -23,6 +30,11 @@ class TransitionImage extends StatefulWidget { required this.animation, required this.thumbnailFit, required this.viewerFit, + this.frameBuilder, + this.loadingBuilder, + this.errorBuilder, + this.semanticLabel, + this.excludeFromSemantics = false, this.width, this.height, this.background, @@ -32,16 +44,33 @@ class TransitionImage extends StatefulWidget { State createState() => _TransitionImageState(); } -class _TransitionImageState extends State { +class _TransitionImageState extends State with WidgetsBindingObserver { ImageStream? _imageStream; ImageInfo? _imageInfo; + ImageChunkEvent? _loadingProgress; bool _isListeningToStream = false; int? _frameNumber; + bool _wasSynchronouslyLoaded = false; + late DisposableBuildContext> _scrollAwareContext; + Object? _lastException; + StackTrace? _lastStack; + ImageStreamCompleterHandle? _completerHandle; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _scrollAwareContext = DisposableBuildContext>(this); + } @override void dispose() { assert(_imageStream != null); + WidgetsBinding.instance.removeObserver(this); _stopListeningToStream(); + _completerHandle?.dispose(); + _scrollAwareContext.dispose(); + _replaceImage(info: null); super.dispose(); } @@ -52,21 +81,23 @@ class _TransitionImageState extends State { if (TickerMode.of(context)) { _listenToStream(); } else { - _stopListeningToStream(); + _stopListeningToStream(keepStreamAlive: true); } super.didChangeDependencies(); } @override - void didUpdateWidget(covariant TransitionImage oldWidget) { + void didUpdateWidget(TransitionImage oldWidget) { super.didUpdateWidget(oldWidget); - if (_isListeningToStream) { + if (_isListeningToStream && (widget.loadingBuilder == null) != (oldWidget.loadingBuilder == null)) { final ImageStreamListener oldListener = _getListener(); _imageStream!.addListener(_getListener(recreateListener: true)); _imageStream!.removeListener(oldListener); } - if (widget.image != oldWidget.image) _resolveImage(); + if (widget.image != oldWidget.image) { + _resolveImage(); + } } @override @@ -76,8 +107,11 @@ class _TransitionImageState extends State { } void _resolveImage() { - final provider = widget.image; - final newStream = provider.resolve(createLocalImageConfiguration( + final ScrollAwareImageProvider provider = ScrollAwareImageProvider( + context: _scrollAwareContext, + imageProvider: widget.image, + ); + final ImageStream newStream = provider.resolve(createLocalImageConfiguration( context, size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null, )); @@ -88,8 +122,26 @@ class _TransitionImageState extends State { ImageStreamListener _getListener({bool recreateListener = false}) { if (_imageStreamListener == null || recreateListener) { + _lastException = null; + _lastStack = null; _imageStreamListener = ImageStreamListener( _handleImageFrame, + onChunk: widget.loadingBuilder == null ? null : _handleImageChunk, + onError: widget.errorBuilder != null || kDebugMode + ? (error, stackTrace) { + setState(() { + _lastException = error; + _lastStack = stackTrace; + }); + assert(() { + if (widget.errorBuilder == null) { + // ignore: only_throw_errors, since we're just proxying the error. + throw error; // Ensures the error message is printed to the console. + } + return true; + }()); + } + : null, ); } return _imageStreamListener!; @@ -98,15 +150,32 @@ class _TransitionImageState extends State { void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) { setState(() { _replaceImage(info: imageInfo); + _loadingProgress = null; + _lastException = null; + _lastStack = null; _frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1; + _wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall; + }); + } + + void _handleImageChunk(ImageChunkEvent event) { + assert(widget.loadingBuilder != null); + setState(() { + _loadingProgress = event; + _lastException = null; + _lastStack = null; }); } void _replaceImage({required ImageInfo? info}) { - _imageInfo?.dispose(); + final ImageInfo? oldImageInfo = _imageInfo; + SchedulerBinding.instance.addPostFrameCallback((_) => oldImageInfo?.dispose()); _imageInfo = info; } + // Updates _imageStream to newStream, and moves the stream listener + // registration from the old stream to the new stream (if a listener was + // registered). void _updateSourceStream(ImageStream newStream) { if (_imageStream?.key == newStream.key) { return; @@ -123,7 +192,9 @@ class _TransitionImageState extends State { } setState(() { + _loadingProgress = null; _frameNumber = null; + _wasSynchronouslyLoaded = false; }); _imageStream = newStream; @@ -138,22 +209,72 @@ class _TransitionImageState extends State { } _imageStream!.addListener(_getListener()); + _completerHandle?.dispose(); + _completerHandle = null; _isListeningToStream = true; } - void _stopListeningToStream() { + /// Stops listening to the image stream, if this state object has attached a + /// listener. + /// + /// If the listener from this state is the last listener on the stream, the + /// stream will be disposed. To keep the stream alive, set `keepStreamAlive` + /// to true, which create [ImageStreamCompleterHandle] to keep the completer + /// alive and is compatible with the [TickerMode] being off. + void _stopListeningToStream({bool keepStreamAlive = false}) { if (!_isListeningToStream) { return; } + if (keepStreamAlive && _completerHandle == null && _imageStream?.completer != null) { + _completerHandle = _imageStream!.completer!.keepAlive(); + } + _imageStream!.removeListener(_getListener()); _isListeningToStream = false; } + Widget _debugBuildErrorWidget(BuildContext context, Object error) { + return Stack( + alignment: Alignment.center, + children: [ + const Positioned.fill( + child: Placeholder( + color: Color(0xCF8D021F), + ), + ), + Padding( + padding: const EdgeInsets.all(4.0), + child: FittedBox( + child: Text( + '$error', + textAlign: TextAlign.center, + textDirection: TextDirection.ltr, + style: const TextStyle( + shadows: [ + Shadow(blurRadius: 1.0), + ], + ), + ), + ), + ), + ], + ); + } + @override Widget build(BuildContext context) { - return ValueListenableBuilder( + if (_lastException != null) { + if (widget.errorBuilder != null) { + return widget.errorBuilder!(context, _lastException!, _lastStack); + } + if (kDebugMode) { + return _debugBuildErrorWidget(context, _lastException!); + } + } + + Widget result = ValueListenableBuilder( valueListenable: widget.animation, builder: (context, t, child) => CustomPaint( painter: _TransitionImagePainter( @@ -166,6 +287,35 @@ class _TransitionImageState extends State { ), ), ); + + if (!widget.excludeFromSemantics) { + result = Semantics( + container: widget.semanticLabel != null, + image: true, + label: widget.semanticLabel ?? '', + child: result, + ); + } + + if (widget.frameBuilder != null) { + result = widget.frameBuilder!(context, result, _frameNumber, _wasSynchronouslyLoaded); + } + + if (widget.loadingBuilder != null) { + result = widget.loadingBuilder!(context, result, _loadingProgress); + } + + return result; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description.add(DiagnosticsProperty('stream', _imageStream)); + description.add(DiagnosticsProperty('pixels', _imageInfo)); + description.add(DiagnosticsProperty('loadingProgress', _loadingProgress)); + description.add(DiagnosticsProperty('frameNumber', _frameNumber)); + description.add(DiagnosticsProperty('wasSynchronouslyLoaded', _wasSynchronouslyLoaded)); } } @@ -193,9 +343,13 @@ class _TransitionImagePainter extends CustomPainter { ..filterQuality = FilterQuality.low; const alignment = Alignment.center; - final rect = ui.Rect.fromLTWH(0, 0, size.width, size.height); - final inputSize = Size(image!.width.toDouble(), image!.height.toDouble()); + final rect = Offset.zero & size; + if (rect.isEmpty) { + return; + } + final outputSize = rect.size; + final inputSize = Size(image!.width.toDouble(), image!.height.toDouble()); final thumbnailSizes = applyBoxFit(thumbnailFit, inputSize / scale, size); final viewerSizes = applyBoxFit(viewerFit, inputSize / scale, size); diff --git a/lib/widgets/common/grid/item_tracker.dart b/lib/widgets/common/grid/item_tracker.dart index 9d657887b..4cd88a32f 100644 --- a/lib/widgets/common/grid/item_tracker.dart +++ b/lib/widgets/common/grid/item_tracker.dart @@ -44,24 +44,18 @@ class _GridItemTrackerState extends State> with WidgetsBin return (scrollableContext.findRenderObject() as RenderBox).size; } - Orientation get _windowOrientation { - final size = WidgetsBinding.instance.window.physicalSize; - return size.width > size.height ? Orientation.landscape : Orientation.portrait; - } - final List _subscriptions = []; // grid section metrics before the app is laid out with the new orientation late SectionedListLayout _lastSectionedListLayout; late Size _lastScrollableSize; - late Orientation _lastOrientation; + Orientation _lastOrientation = Orientation.portrait; @override void initState() { super.initState(); final highlightInfo = context.read(); _subscriptions.add(highlightInfo.eventBus.on>().listen(_trackItem)); - _lastOrientation = _windowOrientation; WidgetsBinding.instance.addObserver(this); _saveLayoutMetrics(); } @@ -78,9 +72,10 @@ class _GridItemTrackerState extends State> with WidgetsBin @override void didChangeMetrics() { // the order of `WidgetsBindingObserver` metrics change notification is unreliable - // w.r.t. the `MediaQuery` update, and consequentially to this widget update: + // w.r.t. the `View` update, and consequentially to this widget update: // `WidgetsBindingObserver` is notified mostly before, sometimes after, the widget update - final orientation = _windowOrientation; + final size = View.of(context).physicalSize; + final orientation = size.width > size.height ? Orientation.landscape : Orientation.portrait; if (_lastOrientation != orientation) { _lastOrientation = orientation; _onLayoutChanged(); @@ -106,7 +101,7 @@ class _GridItemTrackerState extends State> with WidgetsBin final tileRect = sectionedListLayout.getTileRect(event.item); if (tileRect == null) return; - final viewportRect = Rect.fromLTWH(0, scrollController.offset, scrollableSize.width, scrollableSize.height); + final viewportRect = Offset(0, scrollController.offset) & scrollableSize; final itemVisibility = max(0, tileRect.intersect(viewportRect).height) / tileRect.height; if (!event.predicate(itemVisibility)) return; @@ -138,7 +133,7 @@ class _GridItemTrackerState extends State> with WidgetsBin Future _saveLayoutMetrics() async { // use a delay to obtain current layout metrics // so that we can handle window orientation change with the previous metrics, - // regardless of the `MediaQuery`/`WidgetsBindingObserver` order uncertainty + // regardless of the `View`/`WidgetsBindingObserver` order uncertainty await Future.delayed(const Duration(milliseconds: 500)); if (mounted) { diff --git a/lib/widgets/common/grid/scaling.dart b/lib/widgets/common/grid/scaling.dart index 767b86cf9..9f4493a61 100644 --- a/lib/widgets/common/grid/scaling.dart +++ b/lib/widgets/common/grid/scaling.dart @@ -57,7 +57,7 @@ class _GridScaleGestureDetectorState extends State((mq) => mq.gestureSettings); + final gestureSettings = MediaQuery.gestureSettingsOf(context); final child = GestureDetector( // Horizontal/vertical drag gestures are interpreted as scaling @@ -116,11 +116,9 @@ class _GridScaleGestureDetectorState extends State extends State extends State extends State extends State { return _initialized ? BoxDecoration( gradient: RadialGradient( - center: FractionalOffset.fromOffsetAndSize(gradientCenter, context.select((mq) => mq.size)), + center: FractionalOffset.fromOffsetAndSize(gradientCenter, MediaQuery.sizeOf(context)), radius: 1, colors: isDark ? const [ diff --git a/lib/widgets/common/grid/sections/mosaic/row.dart b/lib/widgets/common/grid/sections/mosaic/row.dart index 4d0767f96..8d0aae525 100644 --- a/lib/widgets/common/grid/sections/mosaic/row.dart +++ b/lib/widgets/common/grid/sections/mosaic/row.dart @@ -8,7 +8,7 @@ class MosaicGridRow extends MultiChildRenderObjectWidget { final double spacing; final TextDirection textDirection; - MosaicGridRow({ + const MosaicGridRow({ super.key, required this.rowLayout, required this.spacing, diff --git a/lib/widgets/common/identity/aves_app_bar.dart b/lib/widgets/common/identity/aves_app_bar.dart index 3d5b57559..590be02f1 100644 --- a/lib/widgets/common/identity/aves_app_bar.dart +++ b/lib/widgets/common/identity/aves_app_bar.dart @@ -32,73 +32,68 @@ class AvesAppBar extends StatelessWidget { @override Widget build(BuildContext context) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); final useTvLayout = settings.useTvLayout; - return Selector( - selector: (context, mq) => mq.padding.top, - builder: (context, mqPaddingTop, child) { - return SliverPersistentHeader( - floating: !useTvLayout, - pinned: pinned, - delegate: _SliverAppBarDelegate( - height: mqPaddingTop + appBarHeightForContentHeight(contentHeight), - child: child!, - ), - ); - }, - child: DirectionalSafeArea( - start: !useTvLayout, - bottom: false, - child: AvesFloatingBar( - builder: (context, backgroundColor, child) => Material( - color: backgroundColor, - child: child, - ), - child: Column( - children: [ - SizedBox( - height: kToolbarHeight * context.select((mq) => mq.textScaleFactor), - child: Row( - children: [ - leading != null - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Hero( - tag: leadingHeroTag, - flightShuttleBuilder: _flightShuttleBuilder, - transitionOnUserGestures: true, - child: FontSizeIconTheme( - child: leading!, + return SliverPersistentHeader( + floating: !useTvLayout, + pinned: pinned, + delegate: _SliverAppBarDelegate( + height: MediaQuery.paddingOf(context).top + appBarHeightForContentHeight(contentHeight), + child: DirectionalSafeArea( + start: !useTvLayout, + bottom: false, + child: AvesFloatingBar( + builder: (context, backgroundColor, child) => Material( + color: backgroundColor, + child: child, + ), + child: Column( + children: [ + SizedBox( + height: kToolbarHeight * textScaleFactor, + child: Row( + children: [ + leading != null + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Hero( + tag: leadingHeroTag, + flightShuttleBuilder: _flightShuttleBuilder, + transitionOnUserGestures: true, + child: FontSizeIconTheme( + child: leading!, + ), ), - ), - ) - : const SizedBox(width: 16), - Expanded( - child: DefaultTextStyle( - style: Theme.of(context).appBarTheme.titleTextStyle!, - child: Hero( - tag: titleHeroTag, - flightShuttleBuilder: _flightShuttleBuilder, - transitionOnUserGestures: true, - child: AnimatedSwitcher( - duration: context.read().iconAnimation, - child: FontSizeIconTheme( - child: Row( - key: ValueKey(transitionKey), - children: [ - Expanded(child: title), - ...actions, - ], + ) + : const SizedBox(width: 16), + Expanded( + child: DefaultTextStyle( + style: Theme.of(context).appBarTheme.titleTextStyle!, + child: Hero( + tag: titleHeroTag, + flightShuttleBuilder: _flightShuttleBuilder, + transitionOnUserGestures: true, + child: AnimatedSwitcher( + duration: context.read().iconAnimation, + child: FontSizeIconTheme( + child: Row( + key: ValueKey(transitionKey), + children: [ + Expanded(child: title), + ...actions, + ], + ), ), ), ), ), ), - ), - ], + ], + ), ), - ), - if (bottom != null) bottom!, - ], + if (bottom != null) bottom!, + ], + ), ), ), ), diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 87809c610..1192c6322 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -31,12 +31,12 @@ enum HeroType { always, onTap, never } @immutable class AvesFilterDecoration { - final Widget widget; final Radius radius; + final Widget widget; const AvesFilterDecoration({ - required this.widget, required this.radius, + required this.widget, }); BorderRadius get textBorderRadius => BorderRadius.vertical(bottom: radius); @@ -88,9 +88,9 @@ class AvesFilterChip extends StatefulWidget { required double chipPadding, required double rowPadding, }) { - return context.select((mq) { - return (mq.size.width - mq.padding.horizontal - chipPadding * minChipPerRow - rowPadding) / minChipPerRow; - }); + final mqWidth = MediaQuery.sizeOf(context).width; + final mqHorizontalPadding = MediaQuery.paddingOf(context).horizontal; + return (mqWidth - mqHorizontalPadding - chipPadding * minChipPerRow - rowPadding) / minChipPerRow; } static Future showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async { diff --git a/lib/widgets/common/identity/aves_logo.dart b/lib/widgets/common/identity/aves_logo.dart index c6c03d914..cd25fe4d5 100644 --- a/lib/widgets/common/identity/aves_logo.dart +++ b/lib/widgets/common/identity/aves_logo.dart @@ -37,7 +37,7 @@ class AvesLogo extends StatelessWidget { radius: size / 2, child: CircleAvatar( backgroundColor: Colors.white, - radius: size / 2 - AvesBorder.curvedBorderWidth, + radius: size / 2 - AvesBorder.curvedBorderWidth(context), child: Padding( padding: EdgeInsets.only(top: size / 15), child: child, diff --git a/lib/widgets/common/identity/buttons/overlay_button.dart b/lib/widgets/common/identity/buttons/overlay_button.dart index d4b085433..ce1aeeffd 100644 --- a/lib/widgets/common/identity/buttons/overlay_button.dart +++ b/lib/widgets/common/identity/buttons/overlay_button.dart @@ -73,7 +73,7 @@ class _OverlayButtonState extends State { builder: (context, focused, child) { final border = AvesBorder.border( context, - width: AvesBorder.curvedBorderWidth * (focused ? 3 : 1), + width: AvesBorder.curvedBorderWidth(context) * (focused ? 3 : 1), ); return borderRadius != null ? BlurredRRect( diff --git a/lib/widgets/common/map/buttons/panel.dart b/lib/widgets/common/map/buttons/panel.dart index ecb325cd9..c25427800 100644 --- a/lib/widgets/common/map/buttons/panel.dart +++ b/lib/widgets/common/map/buttons/panel.dart @@ -42,7 +42,6 @@ class MapButtonPanel extends StatelessWidget { tooltip: MaterialLocalizations.of(context).backButtonTooltip, ); } - break; case MapNavigationButton.map: if (openMapPage != null) { navigationButton = MapOverlayButton( @@ -51,7 +50,6 @@ class MapButtonPanel extends StatelessWidget { tooltip: context.l10n.openMapPageTooltip, ); } - break; case MapNavigationButton.none: break; } diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart index 5d008fb20..ef12052ea 100644 --- a/lib/widgets/common/map/geo_map.dart +++ b/lib/widgets/common/map/geo_map.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:math'; -import 'dart:ui'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/images.dart'; @@ -35,6 +34,7 @@ class GeoMap extends StatefulWidget { final AvesMapController? controller; final Listenable? collectionListenable; final List entries; + final Size availableSize; final LatLng? initialCenter; final ValueNotifier isAnimatingNotifier; final ValueNotifier? dotLocationNotifier; @@ -60,6 +60,7 @@ class GeoMap extends StatefulWidget { this.controller, this.collectionListenable, required this.entries, + required this.availableSize, this.initialCenter, required this.isAnimatingNotifier, this.dotLocationNotifier, @@ -133,6 +134,13 @@ class _GeoMapState extends State { @override Widget build(BuildContext context) { + final devicePixelRatio = View.of(context).devicePixelRatio; + void onMarkerLongPress(GeoEntry geoEntry, LatLng tapLocation) => _onMarkerLongPress( + geoEntry: geoEntry, + tapLocation: tapLocation, + devicePixelRatio: devicePixelRatio, + ); + return Selector( selector: (context, s) => s.mapStyle, builder: (context, mapStyle, child) { @@ -143,6 +151,7 @@ class _GeoMapState extends State { buildThumbnailImage: (extent) => ThumbnailImage( entry: key.entry, extent: extent, + devicePixelRatio: devicePixelRatio, progressive: !isHeavy, ), ); @@ -173,9 +182,8 @@ class _GeoMapState extends State { onUserZoomChange: widget.onUserZoomChange, onMapTap: widget.onMapTap, onMarkerTap: _onMarkerTap, - onMarkerLongPress: _onMarkerLongPress, + onMarkerLongPress: onMarkerLongPress, ); - break; case EntryMapStyle.osmHot: case EntryMapStyle.stamenToner: case EntryMapStyle.stamenWatercolor: @@ -204,9 +212,8 @@ class _GeoMapState extends State { onUserZoomChange: widget.onUserZoomChange, onMapTap: widget.onMapTap, onMarkerTap: _onMarkerTap, - onMarkerLongPress: _onMarkerLongPress, + onMarkerLongPress: onMarkerLongPress, ); - break; } } else { final overlay = Center( @@ -360,8 +367,8 @@ class _GeoMapState extends State { ); bounds = bounds.copyWith(zoom: max(minInitialZoom, bounds.zoom.floorToDouble())); - final availableSize = window.physicalSize / window.devicePixelRatio; final neededSize = bounds.toDisplaySize(); + final availableSize = widget.availableSize; if (neededSize.width > availableSize.width || neededSize.height > availableSize.height) { return _initBoundsForEntries(entries: entries, recentCount: (recentCount ?? 10000) ~/ 10); } @@ -457,7 +464,11 @@ class _GeoMapState extends State { onTap(markerLocation, markerEntry); } - Future _onMarkerLongPress(GeoEntry geoEntry, LatLng tapLocation) async { + Future _onMarkerLongPress({ + required GeoEntry geoEntry, + required LatLng tapLocation, + required double devicePixelRatio, + }) async { final onMarkerLongPress = widget.onMarkerLongPress; if (onMarkerLongPress == null) return; @@ -478,6 +489,7 @@ class _GeoMapState extends State { buildThumbnailImage: (extent) => ThumbnailImage( entry: markerEntry, extent: extent, + devicePixelRatio: devicePixelRatio, ), ); onMarkerLongPress( diff --git a/lib/widgets/common/map/leaflet/map.dart b/lib/widgets/common/map/leaflet/map.dart index ec9a5b0a7..a48ff84e6 100644 --- a/lib/widgets/common/map/leaflet/map.dart +++ b/lib/widgets/common/map/leaflet/map.dart @@ -247,8 +247,8 @@ class _EntryLeafletMapState extends State> with TickerProv final bounds = _leafletMapController.bounds; if (bounds != null) { boundsNotifier.value = ZoomedBounds( - sw: bounds.southWest!, - ne: bounds.northEast!, + sw: bounds.southWest, + ne: bounds.northEast, zoom: _leafletMapController.zoom, rotation: _leafletMapController.rotation, ); diff --git a/lib/widgets/common/map/leaflet/scale_layer.dart b/lib/widgets/common/map/leaflet/scale_layer.dart index af99267d9..d9c55d1db 100644 --- a/lib/widgets/common/map/leaflet/scale_layer.dart +++ b/lib/widgets/common/map/leaflet/scale_layer.dart @@ -82,7 +82,6 @@ class ScaleLayerWidget extends StatelessWidget { // meters distanceMeters = scaleMeters[scaleLevel]; displayDistance = distanceMeters >= metersInAKilometer ? '${(distanceMeters / metersInAKilometer).toStringAsFixed(0)} km' : '${distanceMeters.toStringAsFixed(0)} m'; - break; case UnitSystem.imperial: if (scaleLevel < 15) { // miles @@ -95,7 +94,6 @@ class ScaleLayerWidget extends StatelessWidget { distanceMeters = distanceFeet * metersInAFoot; displayDistance = '${distanceFeet.toStringAsFixed(0)} ft'; } - break; } final start = map.project(center); diff --git a/lib/widgets/common/map/leaflet/tile_layers.dart b/lib/widgets/common/map/leaflet/tile_layers.dart index 5ea7d1bb9..2a547a0a3 100644 --- a/lib/widgets/common/map/leaflet/tile_layers.dart +++ b/lib/widgets/common/map/leaflet/tile_layers.dart @@ -1,7 +1,6 @@ import 'package:aves/model/device.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:provider/provider.dart'; const _tileLayerBackgroundColor = Colors.transparent; @@ -14,7 +13,7 @@ class OSMHotLayer extends StatelessWidget { urlTemplate: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', subdomains: const ['a', 'b', 'c'], backgroundColor: _tileLayerBackgroundColor, - retinaMode: context.select((mq) => mq.devicePixelRatio) > 1, + retinaMode: MediaQuery.devicePixelRatioOf(context) > 1, userAgentPackageName: device.userAgent, ); } @@ -29,7 +28,7 @@ class StamenTonerLayer extends StatelessWidget { urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png', subdomains: const ['a', 'b', 'c', 'd'], backgroundColor: _tileLayerBackgroundColor, - retinaMode: context.select((mq) => mq.devicePixelRatio) > 1, + retinaMode: MediaQuery.devicePixelRatioOf(context) > 1, userAgentPackageName: device.userAgent, ); } @@ -44,7 +43,7 @@ class StamenWatercolorLayer extends StatelessWidget { urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg', subdomains: const ['a', 'b', 'c', 'd'], backgroundColor: _tileLayerBackgroundColor, - retinaMode: context.select((mq) => mq.devicePixelRatio) > 1, + retinaMode: MediaQuery.devicePixelRatioOf(context) > 1, userAgentPackageName: device.userAgent, ); } diff --git a/lib/widgets/common/map/map_action_delegate.dart b/lib/widgets/common/map/map_action_delegate.dart index e108d01bb..d5fe1b079 100644 --- a/lib/widgets/common/map/map_action_delegate.dart +++ b/lib/widgets/common/map/map_action_delegate.dart @@ -26,13 +26,10 @@ class MapActionDelegate { ), onSelection: (v) => settings.mapStyle = v, ); - break; case MapAction.zoomIn: controller?.zoomBy(1); - break; case MapAction.zoomOut: controller?.zoomBy(-1); - break; } } } diff --git a/lib/widgets/common/providers/query_provider.dart b/lib/widgets/common/providers/query_provider.dart index 426cb7c56..e811e92ad 100644 --- a/lib/widgets/common/providers/query_provider.dart +++ b/lib/widgets/common/providers/query_provider.dart @@ -3,19 +3,24 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; class QueryProvider extends StatelessWidget { + final bool enabled; final String? initialQuery; final Widget child; const QueryProvider({ super.key, - required this.initialQuery, + this.enabled = false, + this.initialQuery, required this.child, }); @override Widget build(BuildContext context) { return ChangeNotifierProvider( - create: (context) => Query(initialValue: initialQuery), + create: (context) => Query( + enabled: enabled, + initialValue: initialQuery, + ), child: child, ); } diff --git a/lib/widgets/common/search/page.dart b/lib/widgets/common/search/page.dart index 31bfca47d..8aee4f8d9 100644 --- a/lib/widgets/common/search/page.dart +++ b/lib/widgets/common/search/page.dart @@ -114,13 +114,11 @@ class _SearchPageState extends State { key: const ValueKey(SearchBody.suggestions), child: widget.delegate.buildSuggestions(context), ); - break; case SearchBody.results: body = KeyedSubtree( key: const ValueKey(SearchBody.results), child: widget.delegate.buildResults(context), ); - break; case null: break; } diff --git a/lib/widgets/common/thumbnail/decorated.dart b/lib/widgets/common/thumbnail/decorated.dart index b4176b264..e55912e26 100644 --- a/lib/widgets/common/thumbnail/decorated.dart +++ b/lib/widgets/common/thumbnail/decorated.dart @@ -14,7 +14,8 @@ class DecoratedThumbnail extends StatelessWidget { final Object? Function()? heroTagger; static final Color borderColor = Colors.grey.shade700; - static final double borderWidth = AvesBorder.straightBorderWidth; + + static double borderWidth(BuildContext context) => AvesBorder.straightBorderWidth(context); const DecoratedThumbnail({ super.key, @@ -35,6 +36,7 @@ class DecoratedThumbnail extends StatelessWidget { Widget child = ThumbnailImage( entry: entry, extent: tileExtent, + devicePixelRatio: MediaQuery.devicePixelRatioOf(context), isMosaic: isMosaic, cancellableNotifier: cancellableNotifier, heroTag: heroTagger?.call(), @@ -64,7 +66,7 @@ class DecoratedThumbnail extends StatelessWidget { foregroundDecoration: BoxDecoration( border: Border.fromBorderSide(BorderSide( color: borderColor, - width: borderWidth, + width: borderWidth(context), )), ), width: thumbnailWidth, diff --git a/lib/widgets/common/thumbnail/image.dart b/lib/widgets/common/thumbnail/image.dart index 0c3464951..794979ef8 100644 --- a/lib/widgets/common/thumbnail/image.dart +++ b/lib/widgets/common/thumbnail/image.dart @@ -1,5 +1,4 @@ import 'dart:math'; -import 'dart:ui'; import 'package:aves/image_providers/thumbnail_provider.dart'; import 'package:aves/model/entry/entry.dart'; @@ -20,7 +19,7 @@ import 'package:provider/provider.dart'; class ThumbnailImage extends StatefulWidget { final AvesEntry entry; - final double extent; + final double extent, devicePixelRatio; final bool isMosaic, progressive; final BoxFit? fit; final bool showLoadingBackground; @@ -31,6 +30,7 @@ class ThumbnailImage extends StatefulWidget { super.key, required this.entry, required this.extent, + required this.devicePixelRatio, this.progressive = true, this.isMosaic = false, this.fit, @@ -57,7 +57,6 @@ class _ThumbnailImageState extends State { ImageInfo? _lastImageInfo; Object? _lastException; late final ImageStreamListener _streamListener; - late DisposableBuildContext> _scrollAwareContext; AvesEntry get entry => widget.entry; @@ -69,7 +68,6 @@ class _ThumbnailImageState extends State { void initState() { super.initState(); _streamListener = ImageStreamListener(_onImageLoad, onError: _onError); - _scrollAwareContext = DisposableBuildContext>(this); _registerWidget(widget); } @@ -85,7 +83,6 @@ class _ThumbnailImageState extends State { @override void dispose() { _unregisterWidget(widget); - _scrollAwareContext.dispose(); super.dispose(); } @@ -126,16 +123,10 @@ class _ThumbnailImageState extends State { _providers.addAll([ if (lowQuality != null) _ConditionalImageProvider( - ScrollAwareImageProvider( - context: _scrollAwareContext, - imageProvider: lowQuality, - ), + lowQuality, ), _ConditionalImageProvider( - ScrollAwareImageProvider( - context: _scrollAwareContext, - imageProvider: highQuality, - ), + highQuality, _needSizedProvider, ), ]); @@ -176,8 +167,7 @@ class _ThumbnailImageState extends State { bool _needSizedProvider(ImageInfo? currentImageInfo) { if (currentImageInfo == null) return true; final currentImage = currentImageInfo.image; - // directly uses `devicePixelRatio` as it never changes, to avoid visiting ancestors via `MediaQuery` - final sizedThreshold = extent * window.devicePixelRatio; + final sizedThreshold = extent * widget.devicePixelRatio; return sizedThreshold > min(currentImage.width, currentImage.height); } diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 70729be0d..9c5506efb 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -182,18 +182,15 @@ class _AppDebugPageState extends State { }, false); await favourites.clear(); await favourites.add(source.visibleEntries); - break; case AppDebugAction.prepScreenshotStats: settings.changeFilterVisibility(settings.hiddenFilters, true); settings.changeFilterVisibility({ PathFilter('/storage/emulated/0/Pictures/Dev'), }, false); - break; case AppDebugAction.prepScreenshotCountries: settings.changeFilterVisibility({ LocationFilter(LocationLevel.country, 'Belgium;BE'), }, false); - break; case AppDebugAction.mediaStoreScanDir: // scan files copied from test assets // we do it via the app instead of broadcasting via ADB @@ -202,7 +199,6 @@ class _AppDebugPageState extends State { context: context, builder: (context) => const MediaStoreScanDirDialog(), ); - break; case AppDebugAction.greenScreen: await Navigator.maybeOf(context)?.push( MaterialPageRoute( @@ -212,7 +208,6 @@ class _AppDebugPageState extends State { ), ), ); - break; } } } diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart index b03b03356..cbc87137c 100644 --- a/lib/widgets/debug/settings.dart +++ b/lib/widgets/debug/settings.dart @@ -66,7 +66,7 @@ class DebugSettingsSection extends StatelessWidget { 'recentDestinationAlbums': toMultiline(settings.recentDestinationAlbums), 'recentTags': toMultiline(settings.recentTags), 'locale': '${settings.locale}', - 'systemLocales': '${WidgetsBinding.instance.window.locales}', + 'systemLocales': '${WidgetsBinding.instance.platformDispatcher.locales}', 'topEntryIds': '${settings.topEntryIds}', }, ), diff --git a/lib/widgets/dialogs/add_shortcut_dialog.dart b/lib/widgets/dialogs/add_shortcut_dialog.dart index f445e3df6..095a836b8 100644 --- a/lib/widgets/dialogs/add_shortcut_dialog.dart +++ b/lib/widgets/dialogs/add_shortcut_dialog.dart @@ -8,7 +8,6 @@ import 'package:aves/widgets/dialogs/item_picker.dart'; import 'package:aves/widgets/dialogs/pick_dialogs/item_pick_page.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; import 'aves_dialog.dart'; @@ -60,7 +59,7 @@ class _AddShortcutDialogState extends State { return MediaQueryDataProvider( child: Builder( builder: (context) { - final shortestSide = context.select((mq) => mq.size.shortestSide); + final shortestSide = MediaQuery.sizeOf(context).shortestSide; final extent = (shortestSide / 3.0).clamp(60.0, 160.0); return AvesDialog( scrollableContent: [ diff --git a/lib/widgets/dialogs/aves_confirmation_dialog.dart b/lib/widgets/dialogs/aves_confirmation_dialog.dart index ffbba695d..dd9f4b312 100644 --- a/lib/widgets/dialogs/aves_confirmation_dialog.dart +++ b/lib/widgets/dialogs/aves_confirmation_dialog.dart @@ -72,16 +72,12 @@ void _skipConfirmation(ConfirmationDialog type) { switch (type) { case ConfirmationDialog.createVault: settings.confirmCreateVault = false; - break; case ConfirmationDialog.deleteForever: settings.confirmDeleteForever = false; - break; case ConfirmationDialog.moveToBin: settings.confirmMoveToBin = false; - break; case ConfirmationDialog.moveUndatedItems: settings.confirmMoveUndatedItems = false; - break; } } diff --git a/lib/widgets/dialogs/aves_dialog.dart b/lib/widgets/dialogs/aves_dialog.dart index c19c0fe5a..8127a61d1 100644 --- a/lib/widgets/dialogs/aves_dialog.dart +++ b/lib/widgets/dialogs/aves_dialog.dart @@ -3,7 +3,6 @@ import 'dart:ui'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class AvesDialog extends StatelessWidget { static const confirmationRouteName = '/dialog/confirmation'; @@ -104,7 +103,7 @@ class AvesDialog extends StatelessWidget { // workaround because the dialog tries // to size itself to the content intrinsic size, // but the `ListView` viewport does not have one - width: context.select((mq) => mq.size.width / 2), + width: MediaQuery.sizeOf(context).width / 2, child: DecoratedBox( decoration: contentDecoration(context), child: child, diff --git a/lib/widgets/dialogs/convert_entry_dialog.dart b/lib/widgets/dialogs/convert_entry_dialog.dart index f7b047d17..fe9bae4a0 100644 --- a/lib/widgets/dialogs/convert_entry_dialog.dart +++ b/lib/widgets/dialogs/convert_entry_dialog.dart @@ -78,11 +78,9 @@ class _ConvertEntryDialogState extends State { final displaySize = entries.first.displaySize; _widthController.text = '${displaySize.width.round()}'; _heightController.text = '${displaySize.height.round()}'; - break; case LengthUnit.percent: _widthController.text = '100'; _heightController.text = '100'; - break; } } @@ -149,10 +147,8 @@ class _ConvertEntryDialogState extends State { switch (_lengthUnit) { case LengthUnit.px: _heightController.text = '${(width / entries.first.displayAspectRatio).round()}'; - break; case LengthUnit.percent: _heightController.text = '$width'; - break; } } else { _heightController.text = ''; @@ -175,10 +171,8 @@ class _ConvertEntryDialogState extends State { switch (_lengthUnit) { case LengthUnit.px: _widthController.text = '${(height * entries.first.displayAspectRatio).round()}'; - break; case LengthUnit.percent: _widthController.text = '$height'; - break; } } else { _widthController.text = ''; diff --git a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart index f46d2c53c..4111ed044 100644 --- a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart @@ -145,7 +145,7 @@ class _EditEntryDateDialogState extends State { Widget _buildSetCustomContent(BuildContext context) { final l10n = context.l10n; final locale = l10n.localeName; - final use24hour = context.select((v) => v.alwaysUse24HourFormat); + final use24hour = MediaQuery.alwaysUse24HourFormatOf(context); return Padding( padding: const EdgeInsetsDirectional.only(start: 16, end: 8), @@ -179,7 +179,7 @@ class _EditEntryDateDialogState extends State { Widget _buildCopyItemContent(BuildContext context) { final l10n = context.l10n; final locale = l10n.localeName; - final use24hour = context.select((v) => v.alwaysUse24HourFormat); + final use24hour = MediaQuery.alwaysUse24HourFormatOf(context); return Padding( padding: const EdgeInsetsDirectional.only(start: 16, end: 8), @@ -365,11 +365,9 @@ class _EditEntryDateDialogState extends State { case DateEditAction.copyItem: case DateEditAction.extractFromTitle: _isValidNotifier.value = true; - break; case DateEditAction.shift: case DateEditAction.remove: _isValidNotifier.value = _fields.isNotEmpty; - break; } } diff --git a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart index 4096bd154..1cad7d7e4 100644 --- a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart @@ -307,33 +307,26 @@ class _EditEntryLocationDialogState extends State { switch (_action) { case LocationEditAction.chooseOnMap: _isValidNotifier.value = _mapCoordinates != null; - break; case LocationEditAction.copyItem: _isValidNotifier.value = _copyItemSource.hasGps; - break; case LocationEditAction.setCustom: _isValidNotifier.value = _parseLatLng() != null; - break; case LocationEditAction.remove: _isValidNotifier.value = true; - break; } } void _submit(BuildContext context) { + final navigator = Navigator.maybeOf(context); switch (_action) { case LocationEditAction.chooseOnMap: - Navigator.maybeOf(context)?.pop(_mapCoordinates); - break; + navigator?.pop(_mapCoordinates); case LocationEditAction.copyItem: - Navigator.maybeOf(context)?.pop(_copyItemSource.latLng); - break; + navigator?.pop(_copyItemSource.latLng); case LocationEditAction.setCustom: - Navigator.maybeOf(context)?.pop(_parseLatLng()); - break; + navigator?.pop(_parseLatLng()); case LocationEditAction.remove: - Navigator.maybeOf(context)?.pop(ExtraAvesEntryMetadataEdition.removalLocation); - break; + navigator?.pop(ExtraAvesEntryMetadataEdition.removalLocation); } } } diff --git a/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart index 8004d5e1c..4c79a3fb9 100644 --- a/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart @@ -32,11 +32,9 @@ class _EditEntryRatingDialogState extends State { case -1: _action = _RatingAction.rejected; _rating = 0; - break; case 0: _action = _RatingAction.unrated; _rating = 0; - break; default: _action = _RatingAction.set; _rating = entryRating; @@ -121,13 +119,10 @@ class _EditEntryRatingDialogState extends State { switch (_action) { case _RatingAction.set: entryRating = _rating; - break; case _RatingAction.rejected: entryRating = -1; - break; case _RatingAction.unrated: entryRating = 0; - break; } Navigator.maybeOf(context)?.pop(entryRating); } diff --git a/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart b/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart index 3ba035090..a9bb11799 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart @@ -16,7 +16,6 @@ import 'package:aves/widgets/common/identity/buttons/outlined_button.dart'; import 'package:aves/widgets/common/thumbnail/decorated.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:provider/provider.dart'; class RenameEntrySetPage extends StatefulWidget { static const routeName = '/rename_entry_set'; @@ -60,6 +59,8 @@ class _RenameEntrySetPageState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + final effectiveThumbnailExtent = max(thumbnailExtent, thumbnailExtent * textScaleFactor); return AvesScaffold( appBar: AppBar( title: Text(l10n.renameEntrySetPageTitle), @@ -121,63 +122,58 @@ class _RenameEntrySetPageState extends State { ), ), Expanded( - child: Selector( - selector: (context, mq) => mq.textScaleFactor, - builder: (context, textScaleFactor, child) { - final effectiveThumbnailExtent = max(thumbnailExtent, thumbnailExtent * textScaleFactor); - return GridTheme( - extent: effectiveThumbnailExtent, - child: ListView.separated( - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.symmetric(vertical: 12), - itemBuilder: (context, index) { - final entry = entries[index]; - final sourceName = entry.filenameWithoutExtension ?? ''; - return Row( + child: GridTheme( + extent: effectiveThumbnailExtent, + child: ListView.separated( + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(vertical: 12), + itemBuilder: (context, index) { + final entry = entries[index]; + final sourceName = entry.filenameWithoutExtension ?? ''; + return Row( + children: [ + DecoratedThumbnail( + entry: entry, + tileExtent: effectiveThumbnailExtent, + selectable: false, + highlightable: false, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - DecoratedThumbnail( - entry: entry, - tileExtent: effectiveThumbnailExtent, - selectable: false, - highlightable: false, + Text( + sourceName, + style: TextStyle(color: Theme.of(context).textTheme.bodySmall!.color), + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, ), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - sourceName, - style: TextStyle(color: Theme.of(context).textTheme.bodySmall!.color), - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - ), - const SizedBox(height: 4), - ValueListenableBuilder( - valueListenable: _namingPatternNotifier, - builder: (context, pattern, child) { - return Text( - pattern.apply(entry, index), - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - ); - }, - ), - ], - ), + const SizedBox(height: 4), + ValueListenableBuilder( + valueListenable: _namingPatternNotifier, + builder: (context, pattern, child) { + return Text( + pattern.apply(entry, index), + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ); + }, ), ], - ); - }, - separatorBuilder: (context, index) => const SizedBox( - height: CollectionGrid.fixedExtentLayoutSpacing, + ), ), - itemCount: min(entryCount, previewMax), - ), + ], ); - }), + }, + separatorBuilder: (context, index) => const SizedBox( + height: CollectionGrid.fixedExtentLayoutSpacing, + ), + itemCount: min(entryCount, previewMax), + ), + ), ), const Divider(height: 0), Center( diff --git a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart index dbd30f69d..3a3c8db7d 100644 --- a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart @@ -57,7 +57,10 @@ class _CoverSelectionDialogState extends State { static const double itemPickerExtent = 46; static const double appPickerExtent = 32; - double tabBarHeight(BuildContext context) => 64 * max(1, MediaQuery.textScaleFactorOf(context)); + double tabBarHeight(BuildContext context) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + return 64 * max(1, textScaleFactor); + } static const double tabIndicatorWeight = 2; diff --git a/lib/widgets/dialogs/filter_editors/pattern_dialog.dart b/lib/widgets/dialogs/filter_editors/pattern_dialog.dart index f77241c9d..d027af570 100644 --- a/lib/widgets/dialogs/filter_editors/pattern_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/pattern_dialog.dart @@ -2,7 +2,6 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:flutter/material.dart'; import 'package:pattern_lock/pattern_lock.dart'; -import 'package:provider/provider.dart'; class PatternDialog extends StatefulWidget { static const routeName = '/dialog/pattern'; @@ -33,7 +32,7 @@ class _PatternDialogState extends State { Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: SizedBox.square( - dimension: context.select((mq) => mq.size.shortestSide / 2), + dimension: MediaQuery.sizeOf(context).shortestSide / 2, child: PatternLock( relativePadding: .4, selectedColor: colorScheme.secondary, diff --git a/lib/widgets/dialogs/item_picker.dart b/lib/widgets/dialogs/item_picker.dart index 1b908c1f0..c13726e17 100644 --- a/lib/widgets/dialogs/item_picker.dart +++ b/lib/widgets/dialogs/item_picker.dart @@ -43,6 +43,7 @@ class ItemPicker extends StatelessWidget { ThumbnailImage( entry: entry, extent: extent, + devicePixelRatio: MediaQuery.devicePixelRatioOf(context), ), PositionedDirectional( end: -1, diff --git a/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart index 6a74cd192..13f446aef 100644 --- a/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart +++ b/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart @@ -14,6 +14,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/empty.dart'; +import 'package:aves/widgets/common/providers/query_provider.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart'; import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart'; @@ -106,28 +107,31 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { builder: (context, snapshot) { final gridItems = AlbumListPage.getAlbumGridItems(context, source); return SelectionProvider>( - child: FilterGridPage( - settingsRouteKey: AlbumListPage.routeName, - appBar: FilterGridAppBar( - source: source, - title: title, - actionDelegate: AlbumChipSetActionDelegate(gridItems), - actionsBuilder: _buildActions, - isEmpty: false, + child: QueryProvider( + enabled: settings.showAlbumPickQuery, + child: FilterGridPage( + settingsRouteKey: AlbumListPage.routeName, + appBar: FilterGridAppBar( + source: source, + title: title, + actionDelegate: _AlbumChipSetPickActionDelegate(gridItems), + actionsBuilder: _buildActions, + isEmpty: false, + appBarHeightNotifier: _appBarHeightNotifier, + ), appBarHeightNotifier: _appBarHeightNotifier, + sections: AlbumListPage.groupToSections(context, source, gridItems), + newFilters: source.getNewAlbumFilters(context), + sortFactor: settings.albumSortFactor, + showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, + selectable: false, + applyQuery: AlbumListPage.applyQuery, + emptyBuilder: () => EmptyContent( + icon: AIcons.album, + text: context.l10n.albumEmpty, + ), + heroType: HeroType.never, ), - appBarHeightNotifier: _appBarHeightNotifier, - sections: AlbumListPage.groupToSections(context, source, gridItems), - newFilters: source.getNewAlbumFilters(context), - sortFactor: settings.albumSortFactor, - showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, - selectable: false, - applyQuery: AlbumListPage.applyQuery, - emptyBuilder: () => EmptyContent( - icon: AIcons.album, - text: context.l10n.albumEmpty, - ), - heroType: HeroType.never, ), ); }, @@ -160,13 +164,10 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { switch (action) { case ChipSetAction.createAlbum: _createAlbum(); - break; case ChipSetAction.createVault: _createVault(); - break; default: actionDelegate.onActionSelected(context, {}, action); - break; } } @@ -276,3 +277,15 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { Navigator.maybeOf(context)?.pop(filter); } } + +class _AlbumChipSetPickActionDelegate extends AlbumChipSetActionDelegate { + _AlbumChipSetPickActionDelegate(super.items); + + @override + void onActionSelected(BuildContext context, Set filters, ChipSetAction action) { + if (action == ChipSetAction.toggleTitleSearch) { + settings.showAlbumPickQuery = !settings.showAlbumPickQuery; + } + super.onActionSelected(context, filters, action); + } +} diff --git a/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart index a20f76ad4..5c79bc06a 100644 --- a/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart +++ b/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart @@ -20,7 +20,6 @@ import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:latlong2/latlong.dart'; -import 'package:provider/provider.dart'; class LocationPickPage extends StatelessWidget { static const routeName = '/location_pick'; @@ -137,6 +136,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin controller: _mapController, collectionListenable: openingCollection, entries: openingCollection?.sortedEntries ?? [], + availableSize: MediaQuery.sizeOf(context), initialCenter: widget.initialLocation, isAnimatingNotifier: _isPageAnimatingNotifier, dotLocationNotifier: _dotLocationNotifier, @@ -177,12 +177,11 @@ class _LocationInfo extends StatelessWidget { @override Widget build(BuildContext context) { - final orientation = context.select((v) => v.orientation); - return ValueListenableBuilder( valueListenable: locationNotifier, builder: (context, location, child) { - final content = orientation == Orientation.portrait + final isPortrait = MediaQuery.orientationOf(context) == Orientation.portrait; + final content = isPortrait ? [ Expanded( child: Column( @@ -218,7 +217,10 @@ class _LocationInfo extends StatelessWidget { ); } - static double getIconSize(BuildContext context) => 16.0 * context.select((mq) => mq.textScaleFactor); + static double getIconSize(BuildContext context) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + return 16 * textScaleFactor; + } } class _AddressRow extends StatefulWidget { @@ -251,6 +253,7 @@ class _AddressRowState extends State<_AddressRow> { @override Widget build(BuildContext context) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); return Row( mainAxisSize: MainAxisSize.min, children: [ @@ -263,7 +266,7 @@ class _AddressRowState extends State<_AddressRow> { // addresses can include non-latin scripts with inconsistent line height, // which is especially an issue for relayout/painting of heavy Google map, // so we give extra height to give breathing room to the text and stabilize layout - height: Theme.of(context).textTheme.bodyMedium!.fontSize! * context.select((mq) => mq.textScaleFactor) * 2, + height: Theme.of(context).textTheme.bodyMedium!.fontSize! * textScaleFactor * 2, child: ValueListenableBuilder( valueListenable: _addressLineNotifier, builder: (context, addressLine, child) { diff --git a/lib/widgets/dialogs/tile_view_dialog.dart b/lib/widgets/dialogs/tile_view_dialog.dart index b4aa1d56e..941963e98 100644 --- a/lib/widgets/dialogs/tile_view_dialog.dart +++ b/lib/widgets/dialogs/tile_view_dialog.dart @@ -195,8 +195,9 @@ class _TileViewDialogState extends State> with dropdownColor: Themes.thirdLayerColor(context), ); - final iconSize = IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context); - final isPortrait = context.select((mq) => mq.orientation) == Orientation.portrait; + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + final iconSize = IconTheme.of(context).size! * textScaleFactor; + final isPortrait = MediaQuery.orientationOf(context) == Orientation.portrait; final child = isPortrait ? Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/widgets/editor/control_panel.dart b/lib/widgets/editor/control_panel.dart new file mode 100644 index 000000000..2a3dc2711 --- /dev/null +++ b/lib/widgets/editor/control_panel.dart @@ -0,0 +1,137 @@ +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/view/view.dart'; +import 'package:aves/widgets/common/basic/multi_cross_fader.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; +import 'package:aves/widgets/editor/transform/control_panel.dart'; +import 'package:aves/widgets/editor/transform/controller.dart'; +import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class EditorControlPanel extends StatelessWidget { + final AvesEntry entry; + final ValueNotifier actionNotifier; + + static const padding = ViewerButtonRowContent.padding; + static const actions = [ + EditorAction.transform, + ]; + + const EditorControlPanel({ + super.key, + required this.entry, + required this.actionNotifier, + }); + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () { + if (actionNotifier.value != null) { + _cancelAction(context); + return SynchronousFuture(false); + } + return SynchronousFuture(true); + }, + child: Padding( + padding: const EdgeInsets.all(padding), + child: TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, + ), + child: ValueListenableBuilder( + valueListenable: actionNotifier, + builder: (context, action, child) { + return MultiCrossFader( + duration: context.select((v) => v.formTransition), + alignment: Alignment.bottomCenter, + layoutBuilder: (topChild, topChildKey, bottomChild, bottomChildKey) { + return Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + key: bottomChildKey, + left: 0.0, + bottom: 0.0, + right: 0.0, + child: bottomChild, + ), + Positioned( + key: topChildKey, + child: topChild, + ), + ], + ); + }, + child: action == null ? _buildTopLevelPanel(context) : _buildActionPanel(context, action), + ); + }, + ), + ), + ), + ); + } + + Widget _buildTopLevelPanel(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + ...actions.map( + (action) => Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: padding / 2), + child: OverlayButton( + child: IconButton( + icon: action.getIcon(), + onPressed: () => actionNotifier.value = action, + tooltip: action.getText(context), + ), + ), + ), + ), + ], + ), + const SizedBox(height: padding), + Row( + children: [ + const OverlayButton( + child: CloseButton(), + ), + const Spacer(), + OverlayTextButton( + onPressed: () {}, + child: Text(context.l10n.saveCopyButtonLabel), + ), + ], + ), + ], + ); + } + + Widget _buildActionPanel(BuildContext context, EditorAction action) { + switch (action) { + case EditorAction.transform: + return TransformControlPanel( + entry: entry, + onCancel: () => _cancelAction(context), + onApply: (transformation) => _applyAction(context), + ); + } + } + + void _cancelAction(BuildContext context) { + actionNotifier.value = null; + context.read().reset(); + } + + void _applyAction(BuildContext context) { + actionNotifier.value = null; + context.read().reset(); + } +} diff --git a/lib/widgets/editor/entry_editor_page.dart b/lib/widgets/editor/entry_editor_page.dart new file mode 100644 index 000000000..f375aeb27 --- /dev/null +++ b/lib/widgets/editor/entry_editor_page.dart @@ -0,0 +1,136 @@ +import 'dart:async'; + +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/view_state.dart'; +import 'package:aves/widgets/editor/control_panel.dart'; +import 'package:aves/widgets/editor/image.dart'; +import 'package:aves/widgets/editor/transform/controller.dart'; +import 'package:aves/widgets/editor/transform/cropper.dart'; +import 'package:aves/widgets/viewer/overlay/minimap.dart'; +import 'package:aves_magnifier/aves_magnifier.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ImageEditorPage extends StatefulWidget { + static const routeName = '/image_editor'; + + final AvesEntry entry; + + const ImageEditorPage({ + super.key, + required this.entry, + }); + + @override + State createState() => _ImageEditorPageState(); +} + +class _ImageEditorPageState extends State { + final List _subscriptions = []; + final ValueNotifier _actionNotifier = ValueNotifier(null); + final ValueNotifier _paddingNotifier = ValueNotifier(EdgeInsets.zero); + final ValueNotifier _viewStateNotifier = ValueNotifier(ViewState.zero); + final AvesMagnifierController _magnifierController = AvesMagnifierController(); + late final TransformController _transformController; + + @override + void initState() { + super.initState(); + _transformController = TransformController(widget.entry.displaySize); + _actionNotifier.addListener(_onActionChanged); + _subscriptions.add(_transformController.transformationStream.map((v) => v.matrix).distinct().listen(_onTransformationMatrixChanged)); + } + + @override + void dispose() { + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + _actionNotifier.dispose(); + _paddingNotifier.dispose(); + _viewStateNotifier.dispose(); + _magnifierController.dispose(); + _transformController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Provider.value( + value: _transformController, + child: SafeArea( + child: Column( + children: [ + Expanded( + child: Stack( + children: [ + ClipRect( + child: EditorImage( + magnifierController: _magnifierController, + transformController: _transformController, + actionNotifier: _actionNotifier, + paddingNotifier: _paddingNotifier, + viewStateNotifier: _viewStateNotifier, + entry: widget.entry, + ), + ), + if (settings.showOverlayMinimap) + PositionedDirectional( + start: 8, + bottom: 8, + child: Minimap(viewStateNotifier: _viewStateNotifier), + ), + ValueListenableBuilder( + valueListenable: _actionNotifier, + builder: (context, action, child) { + switch (action) { + case EditorAction.transform: + return Cropper( + magnifierController: _magnifierController, + transformController: _transformController, + paddingNotifier: _paddingNotifier, + ); + case null: + return const SizedBox(); + } + }, + ), + ], + ), + ), + EditorControlPanel( + entry: widget.entry, + actionNotifier: _actionNotifier, + ), + ], + ), + ), + ), + resizeToAvoidBottomInset: false, + ); + } + + void _onActionChanged() => _updateImagePadding(); + + void _updateImagePadding() { + if (_actionNotifier.value == EditorAction.transform) { + _paddingNotifier.value = Cropper.imagePadding; + } else { + _paddingNotifier.value = EdgeInsets.zero; + } + } + + void _onTransformationMatrixChanged(Matrix4 transformationMatrix) { + final boundaries = _magnifierController.scaleBoundaries; + if (boundaries != null) { + _magnifierController.setScaleBoundaries( + boundaries.copyWith( + externalTransform: transformationMatrix, + ), + ); + } + } +} diff --git a/lib/widgets/editor/image.dart b/lib/widgets/editor/image.dart new file mode 100644 index 000000000..2b2f703ea --- /dev/null +++ b/lib/widgets/editor/image.dart @@ -0,0 +1,224 @@ +import 'dart:async'; + +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/model/view_state.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/editor/transform/controller.dart'; +import 'package:aves/widgets/editor/transform/painter.dart'; +import 'package:aves/widgets/editor/transform/transformation.dart'; +import 'package:aves/widgets/viewer/visual/error.dart'; +import 'package:aves/widgets/viewer/visual/raster.dart'; +import 'package:aves_magnifier/aves_magnifier.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class EditorImage extends StatefulWidget { + final AvesMagnifierController magnifierController; + final TransformController transformController; + final ValueNotifier actionNotifier; + final ValueNotifier paddingNotifier; + final ValueNotifier viewStateNotifier; + final AvesEntry entry; + + const EditorImage({ + super.key, + required this.magnifierController, + required this.transformController, + required this.actionNotifier, + required this.paddingNotifier, + required this.viewStateNotifier, + required this.entry, + }); + + @override + State createState() => _EditorImageState(); +} + +class _EditorImageState extends State { + final List _subscriptions = []; + final ValueNotifier _scrimOpacityNotifier = ValueNotifier(0); + + AvesEntry get entry => widget.entry; + + TransformController get transformController => widget.transformController; + + ValueNotifier get viewStateNotifier => widget.viewStateNotifier; + + @override + void initState() { + super.initState(); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant EditorImage oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(EditorImage widget) { + widget.actionNotifier.addListener(_onActionChanged); + _subscriptions.add(widget.magnifierController.stateStream.listen(_onViewStateChanged)); + _subscriptions.add(widget.magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged)); + _subscriptions.add(widget.transformController.eventStream.listen(_onTransformEvent)); + } + + void _unregisterWidget(EditorImage widget) { + widget.actionNotifier.removeListener(_onActionChanged); + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + } + + @override + Widget build(BuildContext context) { + return MagnifierGestureDetectorScope( + axis: const [Axis.horizontal, Axis.vertical], + child: StreamBuilder( + stream: transformController.transformationStream, + builder: (context, snapshot) { + final transformation = (snapshot.data ?? Transformation.zero); + final highlightRegionCorners = transformation.region.corners; + final imageToUserMatrix = transformation.matrix; + + final mediaSize = entry.displaySize; + final canvasSize = MatrixUtils.transformRect(imageToUserMatrix, Offset.zero & mediaSize).size; + + return ValueListenableBuilder( + valueListenable: widget.paddingNotifier, + builder: (context, padding, child) { + return Transform( + alignment: Alignment.center, + transform: imageToUserMatrix, + child: ValueListenableBuilder( + valueListenable: widget.actionNotifier, + builder: (context, action, child) { + return LayoutBuilder( + builder: (context, constraints) { + final viewportSize = padding.deflateSize(constraints.biggest); + final minScale = ScaleLevel(factor: ScaleLevel.scaleForContained(viewportSize, canvasSize)); + return AvesMagnifier( + key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'), + controller: widget.magnifierController, + viewportPadding: padding, + contentSize: mediaSize, + allowOriginalScaleBeyondRange: false, + allowGestureScaleBeyondRange: false, + panInertia: _getActionPanInertia(action), + minScale: minScale, + maxScale: const ScaleLevel(factor: 1), + initialScale: minScale, + scaleStateCycle: defaultScaleStateCycle, + applyScale: false, + onScaleStart: (details, doubleTap, boundaries) { + transformController.activity = TransformActivity.pan; + }, + onScaleEnd: (details) { + transformController.activity = TransformActivity.none; + }, + child: child!, + ); + }, + ); + }, + child: Stack( + children: [ + RasterImageView( + entry: entry, + viewStateNotifier: viewStateNotifier, + errorBuilder: (context, error, stackTrace) => ErrorView( + entry: entry, + onTap: () {}, + ), + ), + Positioned.fill( + child: ValueListenableBuilder( + valueListenable: viewStateNotifier, + builder: (context, viewState, child) { + final scale = viewState.scale ?? 1; + final highlightRegionPath = Path()..addPolygon(highlightRegionCorners.map((v) => v * scale).toList(), true); + return ValueListenableBuilder( + valueListenable: _scrimOpacityNotifier, + builder: (context, opacity, child) { + return AnimatedOpacity( + opacity: opacity, + duration: context.read().viewerOverlayAnimation, + child: CustomPaint( + painter: ScrimPainter( + excludePath: highlightRegionPath, + opacity: opacity, + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ); + } + + void _onViewStateChanged(MagnifierState v) { + viewStateNotifier.value = viewStateNotifier.value.copyWith( + position: v.position, + scale: v.scale, + ); + } + + void _onViewScaleBoundariesChanged(ScaleBoundaries v) { + viewStateNotifier.value = viewStateNotifier.value.copyWith( + viewportSize: v.viewportSize, + contentSize: v.contentSize, + ); + } + + void _onActionChanged() => _updateScrim(); + + void _onTransformEvent(TransformEvent event) => _updateScrim(); + + void _updateScrim() => _scrimOpacityNotifier.value = _getActionScrimOpacity(widget.actionNotifier.value, transformController.activity); + + static double _getActionPanInertia(EditorAction? action) { + switch (action) { + case EditorAction.transform: + return 0; + case null: + return AvesMagnifier.defaultPanInertia; + } + } + + static double _getActionScrimOpacity(EditorAction? action, TransformActivity activity) { + switch (action) { + case EditorAction.transform: + switch (activity) { + case TransformActivity.none: + return .9; + case TransformActivity.pan: + case TransformActivity.resize: + case TransformActivity.straighten: + return .6; + } + case null: + return 0; + } + } +} diff --git a/lib/widgets/editor/transform/control_panel.dart b/lib/widgets/editor/transform/control_panel.dart new file mode 100644 index 000000000..4615563bd --- /dev/null +++ b/lib/widgets/editor/transform/control_panel.dart @@ -0,0 +1,196 @@ +import 'package:aves/model/entry/entry.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/view/view.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; +import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; +import 'package:aves/widgets/editor/control_panel.dart'; +import 'package:aves/widgets/editor/transform/controller.dart'; +import 'package:aves/widgets/editor/transform/transformation.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; + +class TransformControlPanel extends StatefulWidget { + final AvesEntry entry; + final VoidCallback onCancel; + final void Function(Transformation transformation) onApply; + + const TransformControlPanel({ + super.key, + required this.entry, + required this.onCancel, + required this.onApply, + }); + + @override + State createState() => _TransformControlPanelState(); +} + +class _TransformControlPanelState extends State with TickerProviderStateMixin { + late final List> _tabs; + late final TabController _tabController; + + static const padding = EditorControlPanel.padding; + + @override + void initState() { + super.initState(); + _tabs = [ + Tuple2( + (context) => Tab(text: context.l10n.editorTransformCrop), + (context) => const CropControlPanel(), + ), + Tuple2( + (context) => Tab(text: context.l10n.editorTransformRotate), + (context) => const RotationControlPanel(), + ), + ]; + _tabController = TabController( + length: _tabs.length, + vsync: this, + ); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final transformController = context.watch(); + return Column( + children: [ + SizedBox( + height: CropControlPanel.preferredHeight(context), + child: AnimatedBuilder( + animation: _tabController, + builder: (context, child) { + return AnimatedSwitcher( + duration: context.select((v) => v.formTransition), + child: _tabs[_tabController.index].item2(context), + ); + }, + ), + ), + const SizedBox(height: padding), + Row( + children: [ + const OverlayButton( + child: BackButton(), + ), + Expanded( + child: TabBar( + tabs: _tabs.map((v) => v.item1(context)).toList(), + controller: _tabController, + padding: const EdgeInsets.symmetric(horizontal: padding), + indicatorSize: TabBarIndicatorSize.label, + ), + ), + OverlayButton( + child: StreamBuilder( + stream: transformController.transformationStream, + builder: (context, snapshot) { + return IconButton( + icon: const Icon(AIcons.apply), + onPressed: transformController.modified ? () => widget.onApply(transformController.transformation) : null, + tooltip: context.l10n.applyTooltip, + ); + }, + ), + ), + ], + ), + ], + ); + } +} + +class CropControlPanel extends StatelessWidget { + const CropControlPanel({super.key}); + + static double preferredHeight(BuildContext context) => CropAspectRatio.values.map((v) { + return CaptionedButton.getSize(context, v.getText(context), showCaption: true).height; + }).max; + + @override + Widget build(BuildContext context) { + final aspectRatioNotifier = context.select>((v) => v.aspectRatioNotifier); + return ListView.builder( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemBuilder: (context, index) { + final ratio = CropAspectRatio.values[index]; + void setAspectRatio() => aspectRatioNotifier.value = ratio; + return CaptionedButton( + iconButtonBuilder: (context, focusNode) { + return ValueListenableBuilder( + valueListenable: aspectRatioNotifier, + builder: (context, selectedRatio, child) { + return IconButton( + color: ratio == selectedRatio ? Theme.of(context).colorScheme.primary : null, + onPressed: setAspectRatio, + focusNode: focusNode, + icon: ratio.getIcon(), + ); + }, + ); + }, + caption: ratio.getText(context), + onPressed: setAspectRatio, + ); + }, + itemCount: CropAspectRatio.values.length, + ); + } +} + +class RotationControlPanel extends StatelessWidget { + const RotationControlPanel({super.key}); + + @override + Widget build(BuildContext context) { + final controller = context.watch(); + + return Row( + children: [ + _buildButton(context, EntryAction.flip, controller.flipHorizontally), + Expanded( + child: StreamBuilder( + stream: controller.transformationStream, + builder: (context, snapshot) { + final transformation = snapshot.data ?? Transformation.zero; + return Slider( + value: transformation.straightenDegrees, + min: TransformController.straightenDegreesMin, + max: TransformController.straightenDegreesMax, + divisions: 18, + onChangeStart: (v) => controller.activity = TransformActivity.straighten, + onChangeEnd: (v) => controller.activity = TransformActivity.none, + label: NumberFormat('0.0°', context.l10n.localeName).format(transformation.straightenDegrees), + onChanged: (v) => controller.straightenDegrees = v, + ); + }, + ), + ), + _buildButton(context, EntryAction.rotateCW, controller.rotateClockwise), + ], + ); + } + + Widget _buildButton(BuildContext context, EntryAction action, VoidCallback onPressed) { + return OverlayButton( + child: IconButton( + icon: action.getIcon(), + onPressed: onPressed, + tooltip: action.getText(context), + ), + ); + } +} diff --git a/lib/widgets/editor/transform/controller.dart b/lib/widgets/editor/transform/controller.dart new file mode 100644 index 000000000..0c414a326 --- /dev/null +++ b/lib/widgets/editor/transform/controller.dart @@ -0,0 +1,88 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:aves/widgets/editor/transform/crop_region.dart'; +import 'package:aves/widgets/editor/transform/transformation.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:flutter/foundation.dart'; + +class TransformController { + ValueNotifier aspectRatioNotifier = ValueNotifier(CropAspectRatio.free); + + TransformActivity _activity = TransformActivity.none; + + TransformActivity get activity => _activity; + + Transformation _transformation = Transformation.zero; + + Transformation get transformation => _transformation; + + bool get modified => _transformation != Transformation.zero; + + final StreamController _transformationStreamController = StreamController.broadcast(); + + Stream get transformationStream => _transformationStreamController.stream; + + final StreamController _eventStreamController = StreamController.broadcast(); + + Stream get eventStream => _eventStreamController.stream; + + static const double straightenDegreesMin = -45; + static const double straightenDegreesMax = 45; + + final Size displaySize; + + TransformController(this.displaySize) { + reset(); + aspectRatioNotifier.addListener(_onAspectRatioChanged); + } + + void dispose() { + aspectRatioNotifier.dispose(); + } + + void reset() { + _transformation = Transformation.zero.copyWith( + region: CropRegion.fromRect(Offset.zero & displaySize), + ); + _transformationStreamController.add(_transformation); + } + + void flipHorizontally() { + _transformation = _transformation.copyWith( + orientation: _transformation.orientation.flipHorizontally(), + straightenDegrees: -transformation.straightenDegrees, + ); + _transformationStreamController.add(_transformation); + } + + void rotateClockwise() { + _transformation = _transformation.copyWith( + orientation: _transformation.orientation.rotateClockwise(), + ); + _transformationStreamController.add(_transformation); + } + + set straightenDegrees(double straightenDegrees) { + _transformation = _transformation.copyWith( + straightenDegrees: straightenDegrees.clamp(straightenDegreesMin, straightenDegreesMax), + ); + _transformationStreamController.add(_transformation); + } + + set cropRegion(CropRegion region) { + _transformation = _transformation.copyWith( + region: region, + ); + _transformationStreamController.add(_transformation); + } + + set activity(TransformActivity activity) { + _activity = activity; + _eventStreamController.add(TransformEvent(activity: _activity)); + } + + void _onAspectRatioChanged() { + // TODO TLAD [crop] apply + } +} diff --git a/lib/widgets/editor/transform/crop_region.dart b/lib/widgets/editor/transform/crop_region.dart new file mode 100644 index 000000000..013a12c9a --- /dev/null +++ b/lib/widgets/editor/transform/crop_region.dart @@ -0,0 +1,48 @@ +import 'dart:ui'; + +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; + +@immutable +class CropRegion extends Equatable { + final Offset topLeft, topRight, bottomRight, bottomLeft; + + List get corners => [topLeft, topRight, bottomRight, bottomLeft]; + + Offset get center => (topLeft + bottomRight) / 2; + + Rect get outsideRect { + final xMin = corners.map((v) => v.dx).min; + final xMax = corners.map((v) => v.dx).max; + final yMin = corners.map((v) => v.dy).min; + final yMax = corners.map((v) => v.dy).max; + return Rect.fromPoints(Offset(xMin, yMin), Offset(xMax, yMax)); + } + + @override + List get props => [topLeft, topRight, bottomRight, bottomLeft]; + + const CropRegion({ + required this.topLeft, + required this.topRight, + required this.bottomRight, + required this.bottomLeft, + }); + + static const CropRegion zero = CropRegion( + topLeft: Offset.zero, + topRight: Offset.zero, + bottomRight: Offset.zero, + bottomLeft: Offset.zero, + ); + + factory CropRegion.fromRect(Rect rect) { + return CropRegion( + topLeft: rect.topLeft, + topRight: rect.topRight, + bottomRight: rect.bottomRight, + bottomLeft: rect.bottomLeft, + ); + } +} diff --git a/lib/widgets/editor/transform/cropper.dart b/lib/widgets/editor/transform/cropper.dart new file mode 100644 index 000000000..df31cc44b --- /dev/null +++ b/lib/widgets/editor/transform/cropper.dart @@ -0,0 +1,734 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:aves/model/view_state.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/extensions/geometry.dart'; +import 'package:aves/widgets/common/fx/dashed_path_painter.dart'; +import 'package:aves/widgets/editor/transform/controller.dart'; +import 'package:aves/widgets/editor/transform/crop_region.dart'; +import 'package:aves/widgets/editor/transform/handles.dart'; +import 'package:aves/widgets/editor/transform/painter.dart'; +import 'package:aves/widgets/editor/transform/transformation.dart'; +import 'package:aves_magnifier/aves_magnifier.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:aves_utils/aves_utils.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class Cropper extends StatefulWidget { + final AvesMagnifierController magnifierController; + final TransformController transformController; + final ValueNotifier paddingNotifier; + + static const double handleDimension = kMinInteractiveDimension; + static const EdgeInsets imagePadding = EdgeInsets.all(kMinInteractiveDimension); + + const Cropper({ + super.key, + required this.magnifierController, + required this.transformController, + required this.paddingNotifier, + }); + + @override + State createState() => _CropperState(); +} + +class _CropperState extends State with SingleTickerProviderStateMixin { + final List _subscriptions = []; + final ValueNotifier _viewportSizeNotifier = ValueNotifier(Size.zero); + final ValueNotifier _outlineNotifier = ValueNotifier(Rect.zero); + final ValueNotifier _gridDivisionNotifier = ValueNotifier(0); + late AnimationController _gridAnimationController; + late Animation _gridOpacity; + + static const double minDimension = Cropper.handleDimension; + static const int panResizeGridDivision = 3; + static const int straightenGridDivision = 9; + static const double overOutlineFactor = .25; + + AvesMagnifierController get magnifierController => widget.magnifierController; + + TransformController get transformController => widget.transformController; + + Transformation get transformation => transformController.transformation; + + CropAspectRatio get cropAspectRatio => transformController.aspectRatioNotifier.value; + + @override + void initState() { + super.initState(); + final initialRegion = transformation.region; + _viewportSizeNotifier.addListener(() => _initOutline(initialRegion)); + _gridAnimationController = AnimationController( + duration: context.read().viewerOverlayAnimation, + vsync: this, + ); + _gridOpacity = CurvedAnimation( + parent: _gridAnimationController, + curve: Curves.easeOutQuad, + ); + _registerWidget(widget); + _initOutline(initialRegion); + } + + @override + void didUpdateWidget(covariant Cropper oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _viewportSizeNotifier.dispose(); + _outlineNotifier.dispose(); + _gridDivisionNotifier.dispose(); + _gridAnimationController.dispose(); + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(Cropper widget) { + _subscriptions.add(widget.magnifierController.stateStream.listen(_onViewStateChanged)); + _subscriptions.add(widget.magnifierController.scaleBoundariesStream.listen(_onViewBoundariesChanged)); + _subscriptions.add(widget.transformController.eventStream.listen(_onTransformEvent)); + _subscriptions.add(widget.transformController.transformationStream.map((v) => v.orientation).distinct().listen(_onOrientationChanged)); + _subscriptions.add(widget.transformController.transformationStream.map((v) => v.straightenDegrees).distinct().listen(_onStraightenDegreesChanged)); + widget.transformController.aspectRatioNotifier.addListener(_onCropAspectRatioChanged); + } + + void _unregisterWidget(Cropper widget) { + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + widget.transformController.aspectRatioNotifier.removeListener(_onCropAspectRatioChanged); + } + + @override + Widget build(BuildContext context) { + return Positioned.fill( + child: ValueListenableBuilder( + valueListenable: widget.paddingNotifier, + builder: (context, padding, child) { + return ValueListenableBuilder( + valueListenable: _outlineNotifier, + builder: (context, outline, child) { + if (outline.isEmpty) return const SizedBox(); + + final outlineVisualRect = outline.translate(padding.left, padding.top); + return Stack( + children: [ + Positioned.fill( + child: IgnorePointer( + child: Stack( + children: [ + _buildDashLine([outlineVisualRect.topLeft, outlineVisualRect.topRight]), + _buildDashLine([outlineVisualRect.bottomLeft, outlineVisualRect.bottomRight]), + _buildDashLine([outlineVisualRect.topLeft, outlineVisualRect.bottomLeft]), + _buildDashLine([outlineVisualRect.topRight, outlineVisualRect.bottomRight]), + Positioned.fill( + child: ValueListenableBuilder( + valueListenable: _gridDivisionNotifier, + builder: (context, gridDivision, child) { + return ValueListenableBuilder( + valueListenable: _gridOpacity, + builder: (context, gridOpacity, child) { + return CustomPaint( + painter: CropperPainter( + rect: outlineVisualRect, + gridOpacity: gridOpacity, + gridDivision: gridDivision, + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ), + _buildVertexHandle( + padding: padding, + getPosition: () => outline.topLeft, + setPosition: (v) => _handleOutline( + topLeft: Offset(min(outline.right - minDimension, v.dx), min(outline.bottom - minDimension, v.dy)), + ), + ), + _buildVertexHandle( + padding: padding, + getPosition: () => outline.topRight, + setPosition: (v) => _handleOutline( + topRight: Offset(max(outline.left + minDimension, v.dx), min(outline.bottom - minDimension, v.dy)), + ), + ), + _buildVertexHandle( + padding: padding, + getPosition: () => outline.bottomRight, + setPosition: (v) => _handleOutline( + bottomRight: Offset(max(outline.left + minDimension, v.dx), max(outline.top + minDimension, v.dy)), + ), + ), + _buildVertexHandle( + padding: padding, + getPosition: () => outline.bottomLeft, + setPosition: (v) => _handleOutline( + bottomLeft: Offset(min(outline.right - minDimension, v.dx), max(outline.top + minDimension, v.dy)), + ), + ), + _buildEdgeHandle( + padding: padding, + getEdge: () => Rect.fromPoints(outline.bottomLeft, outline.topLeft), + setEdge: (v) { + final left = min(outline.right - minDimension, v.left); + return _handleOutline( + topLeft: Offset(left, outline.top), + bottomLeft: Offset(left, outline.bottom), + ); + }, + ), + _buildEdgeHandle( + padding: padding, + getEdge: () => Rect.fromPoints(outline.topLeft, outline.topRight), + setEdge: (v) { + final top = min(outline.bottom - minDimension, v.top); + return _handleOutline( + topLeft: Offset(outline.left, top), + topRight: Offset(outline.right, top), + ); + }, + ), + _buildEdgeHandle( + padding: padding, + getEdge: () => Rect.fromPoints(outline.bottomRight, outline.topRight), + setEdge: (v) { + final right = max(outline.left + minDimension, v.right); + return _handleOutline( + topRight: Offset(right, outline.top), + bottomRight: Offset(right, outline.bottom), + ); + }, + ), + _buildEdgeHandle( + padding: padding, + getEdge: () => Rect.fromPoints(outline.bottomLeft, outline.bottomRight), + setEdge: (v) { + final bottom = max(outline.top + minDimension, v.bottom); + return _handleOutline( + bottomLeft: Offset(outline.left, bottom), + bottomRight: Offset(outline.right, bottom), + ); + }, + ), + ], + ); + }, + ); + }, + ), + ); + } + + // use 1 painter per line so that the dashes of one line + // do not get offset depending on the previous line length + Widget _buildDashLine(List points) => CustomPaint( + painter: DashedPathPainter( + originalPath: Path()..addPolygon(points, false), + pathColor: CropperPainter.borderColor, + strokeWidth: CropperPainter.borderWidth, + ), + ); + + void _handleOutline({ + Offset? topLeft, + Offset? topRight, + Offset? bottomRight, + Offset? bottomLeft, + }) { + final currentOutline = _outlineNotifier.value; + var targetOutline = Rect.fromLTRB( + topLeft?.dx ?? bottomLeft?.dx ?? currentOutline.left, + topLeft?.dy ?? topRight?.dy ?? currentOutline.top, + topRight?.dx ?? bottomRight?.dx ?? currentOutline.right, + bottomLeft?.dy ?? bottomRight?.dy ?? currentOutline.bottom, + ); + + _RatioStrategy? ratioStrategy; + if (topLeft != null && topRight != null) { + ratioStrategy = _RatioStrategy.pinBottom; + } else if (topRight != null && bottomRight != null) { + ratioStrategy = _RatioStrategy.pinLeft; + } else if (bottomLeft != null && bottomRight != null) { + ratioStrategy = _RatioStrategy.pinTop; + } else if (topLeft != null && bottomLeft != null) { + ratioStrategy = _RatioStrategy.pinRight; + } else if (topLeft != null) { + ratioStrategy = _RatioStrategy.pinBottomRight; + } else if (topRight != null) { + ratioStrategy = _RatioStrategy.pinBottomLeft; + } else if (bottomRight != null) { + ratioStrategy = _RatioStrategy.pinTopLeft; + } else if (bottomLeft != null) { + ratioStrategy = _RatioStrategy.pinTopRight; + } + if (ratioStrategy != null) { + targetOutline = _applyCropRatioToOutline(targetOutline, ratioStrategy); + } + + // do not try to coerce outline handled outside tilted image + if (transformation.straightenDegrees != 0 && !_isOutlineContained(targetOutline)) return; + + // dismiss if we could not honour aspect ratio + if (cropAspectRatio != CropAspectRatio.free && !_isOutlineContained(targetOutline)) return; + + final currentState = _getViewState(); + final boundaries = magnifierController.scaleBoundaries; + if (currentState == null || boundaries == null) return; + + final gestureRegion = _regionFromOutline(currentState, targetOutline); + final viewportSize = boundaries.viewportSize; + + final gestureOutline = _regionToContainedOutline(currentState, gestureRegion); + final clampedOutline = Rect.fromLTRB( + max(gestureOutline.left, 0), + max(gestureOutline.top, 0), + min(gestureOutline.right, viewportSize.width), + min(gestureOutline.bottom, viewportSize.height), + ); + _setOutline(clampedOutline); + _updateCropRegion(); + + // zoom out when user gesture reaches outer edges + + if (gestureOutline.width - clampedOutline.width > precisionErrorTolerance || gestureOutline.height - clampedOutline.height > precisionErrorTolerance) { + final targetOutline = Rect.lerp(clampedOutline, gestureOutline, overOutlineFactor)!; + final targetRegion = _regionFromOutline(currentState, targetOutline); + + final nextState = _viewStateForContainedRegion(boundaries, targetRegion); + if (nextState != currentState) { + magnifierController.update( + position: nextState.position, + scale: nextState.scale, + source: ChangeSource.animation, + ); + _setOutline(_regionToContainedOutline(nextState, targetRegion)); + } + } + } + + bool _isOutlineContained(Rect outline) { + final currentState = _getViewState(); + final boundaries = magnifierController.scaleBoundaries; + if (currentState == null || boundaries == null) return false; + + final regionToOutlineMatrix = _getRegionToOutlineMatrix(currentState); + final outlineToRegionMatrix = Matrix4.inverted(regionToOutlineMatrix); + final regionCorners = { + outline.topLeft, + outline.topRight, + outline.bottomRight, + outline.bottomLeft, + }.map(outlineToRegionMatrix.transformOffset).toSet(); + + final contentRect = Offset.zero & boundaries.contentSize; + return regionCorners.every((v) => contentRect.containsIncludingBottomRight(v, tolerance: precisionErrorTolerance)); + } + + VertexHandle _buildVertexHandle({ + required EdgeInsets padding, + required ValueGetter getPosition, + required ValueSetter setPosition, + }) { + return VertexHandle( + padding: padding, + getPosition: getPosition, + setPosition: setPosition, + onDragStart: _onDragStart, + onDragEnd: _onDragEnd, + ); + } + + EdgeHandle _buildEdgeHandle({ + required EdgeInsets padding, + required ValueGetter getEdge, + required ValueSetter setEdge, + }) { + return EdgeHandle( + padding: padding, + getEdge: getEdge, + setEdge: setEdge, + onDragStart: _onDragStart, + onDragEnd: _onDragEnd, + ); + } + + void _onDragStart() { + transformController.activity = TransformActivity.resize; + } + + void _onDragEnd() { + transformController.activity = TransformActivity.none; + _showRegion(); + } + + void _showRegion() { + final boundaries = magnifierController.scaleBoundaries; + if (boundaries == null) return; + + final region = transformation.region; + final nextState = _viewStateForContainedRegion(boundaries, region); + + magnifierController.update( + position: nextState.position, + scale: nextState.scale, + source: ChangeSource.animation, + ); + _setOutline(_regionToContainedOutline(nextState, region)); + } + + ViewState _viewStateForContainedRegion(ScaleBoundaries boundaries, CropRegion region) { + final regionSize = MatrixUtils.transformRect(transformation.matrix, region.outsideRect).size; + final nextScale = boundaries.clampScale(ScaleLevel.scaleForContained(boundaries.viewportSize, regionSize)); + final nextPosition = boundaries.clampPosition( + position: boundaries.contentToStatePosition(nextScale, region.center), + scale: nextScale, + ); + return ViewState( + position: nextPosition, + scale: nextScale, + viewportSize: boundaries.viewportSize, + contentSize: boundaries.contentSize, + ); + } + + void _onTransformEvent(TransformEvent event) { + final activity = event.activity; + switch (activity) { + case TransformActivity.none: + break; + case TransformActivity.pan: + case TransformActivity.resize: + _gridDivisionNotifier.value = panResizeGridDivision; + break; + case TransformActivity.straighten: + _gridDivisionNotifier.value = straightenGridDivision; + break; + } + if (activity == TransformActivity.none) { + _gridAnimationController.reverse(); + } else { + _gridAnimationController.forward(); + } + } + + void _onOrientationChanged(TransformOrientation orientation) { + _showRegion(); + } + + void _onStraightenDegreesChanged(double degrees) { + _updateCropRegion(); + } + + void _onCropAspectRatioChanged() { + final viewState = _getViewState(); + if (viewState == null) return; + + var targetOutline = _applyCropRatioToOutline(_outlineNotifier.value, _RatioStrategy.keepArea); + if (!_isOutlineContained(targetOutline)) { + targetOutline = _applyCropRatioToOutline(_outlineNotifier.value, _RatioStrategy.contain); + } + transformController.cropRegion = _regionFromOutline(viewState, targetOutline); + _showRegion(); + } + + void _onViewStateChanged(MagnifierState state) { + final currentOutline = _outlineNotifier.value; + switch (state.source) { + case ChangeSource.internal: + case ChangeSource.animation: + _setOutline(currentOutline); + break; + case ChangeSource.gesture: + // TODO TLAD [crop] use other strat + _setOutline(_applyCropRatioToOutline(currentOutline, _RatioStrategy.contain)); + _updateCropRegion(); + break; + } + } + + void _onViewBoundariesChanged(ScaleBoundaries scaleBoundaries) { + _viewportSizeNotifier.value = scaleBoundaries.viewportSize; + } + + ViewState? _getViewState() { + final scaleBoundaries = magnifierController.scaleBoundaries; + if (scaleBoundaries == null) return null; + + final state = magnifierController.currentState; + return ViewState( + position: state.position, + scale: state.scale, + viewportSize: scaleBoundaries.viewportSize, + contentSize: scaleBoundaries.contentSize, + ); + } + + void _initOutline(CropRegion region) { + final viewState = _getViewState(); + if (viewState != null) { + _setOutline(_regionToContainedOutline(viewState, region)); + _updateCropRegion(); + } + } + + void _setOutline(Rect targetOutline) { + final viewState = _getViewState(); + final viewportSize = viewState?.viewportSize; + if (targetOutline.isEmpty || viewState == null || viewportSize == null) return; + + // ensure outline is within content + final targetRegion = _regionFromOutline(viewState, targetOutline); + var newOutline = _regionToContainedOutline(viewState, targetRegion); + + // ensure outline is large enough to be handled + newOutline = Rect.fromLTWH( + newOutline.left, + newOutline.top, + max(newOutline.width, minDimension), + max(newOutline.height, minDimension), + ); + + // ensure outline is within viewport + newOutline = Rect.fromLTRB( + max(newOutline.left, 0), + max(newOutline.top, 0), + min(newOutline.right, viewportSize.width), + min(newOutline.bottom, viewportSize.height), + ); + + _outlineNotifier.value = newOutline; + } + + void _updateCropRegion() { + final viewState = _getViewState(); + final outline = _outlineNotifier.value; + if (viewState != null && !outline.isEmpty) { + transformController.cropRegion = _regionFromOutline(viewState, outline); + } + } + + Matrix4 _getRegionToOutlineMatrix(ViewState viewState) { + final magnifierMatrix = viewState.matrix; + + final viewportCenter = viewState.viewportSize!.center(Offset.zero); + final transformOrigin = Matrix4.inverted(magnifierMatrix).transformOffset(viewportCenter); + final transformMatrix = Matrix4.identity() + ..translate(transformOrigin.dx, transformOrigin.dy) + ..multiply(transformation.matrix) + ..translate(-transformOrigin.dx, -transformOrigin.dy); + + return magnifierMatrix..multiply(transformMatrix); + } + + CropRegion _regionFromOutline(ViewState viewState, Rect outline) { + final regionToOutlineMatrix = _getRegionToOutlineMatrix(viewState); + final outlineToRegionMatrix = regionToOutlineMatrix..invert(); + + final region = CropRegion( + topLeft: outlineToRegionMatrix.transformOffset(outline.topLeft), + topRight: outlineToRegionMatrix.transformOffset(outline.topRight), + bottomRight: outlineToRegionMatrix.transformOffset(outline.bottomRight), + bottomLeft: outlineToRegionMatrix.transformOffset(outline.bottomLeft), + ); + + final rect = Offset.zero & viewState.contentSize!; + double clampX(double dx) => dx.clamp(rect.left, rect.right); + double clampY(double dy) => dy.clamp(rect.top, rect.bottom); + Offset clampPoint(Offset v) => Offset(clampX(v.dx), clampY(v.dy)); + final clampedRegion = CropRegion( + topLeft: clampPoint(region.topLeft), + topRight: clampPoint(region.topRight), + bottomRight: clampPoint(region.bottomRight), + bottomLeft: clampPoint(region.bottomLeft), + ); + return clampedRegion; + } + + Rect _regionToContainedOutline(ViewState viewState, CropRegion region) { + final matrix = _getRegionToOutlineMatrix(viewState); + final points = region.corners.map(matrix.transformOffset).toSet(); + final sortedX = points.map((v) => v.dx).toList()..sort(); + final sortedY = points.map((v) => v.dy).toList()..sort(); + final topLeft = Offset(sortedX[1], sortedY[1]); + final bottomRight = Offset(sortedX[2], sortedY[2]); + return Rect.fromPoints(topLeft, bottomRight); + } + + Rect _applyCropRatioToOutline(Rect outline, _RatioStrategy strategy) { + final currentState = _getViewState(); + final boundaries = magnifierController.scaleBoundaries; + if (currentState == null || boundaries == null) return outline; + + final contentSize = boundaries.contentSize; + + late int longCoef; + late int shortCoef; + switch (cropAspectRatio) { + case CropAspectRatio.free: + return outline; + case CropAspectRatio.original: + longCoef = contentSize.longestSide.round(); + shortCoef = contentSize.shortestSide.round(); + break; + case CropAspectRatio.square: + longCoef = 1; + shortCoef = 1; + break; + case CropAspectRatio.ar_16_9: + longCoef = 16; + shortCoef = 9; + break; + case CropAspectRatio.ar_4_3: + longCoef = 4; + shortCoef = 3; + break; + } + + final contentRect = Offset.zero & contentSize; + final isLandscape = (outline.width - outline.height).abs() > precisionErrorTolerance ? outline.width > outline.height : contentSize.width > contentSize.height; + final newRatio = isLandscape ? longCoef / shortCoef : shortCoef / longCoef; + + Size sizeToKeepArea() { + final f = (outline.longestSide + outline.shortestSide) / (longCoef + shortCoef); + final newLongest = f * longCoef; + final newShortest = f * shortCoef; + return isLandscape ? Size(newLongest, newShortest) : Size(newShortest, newLongest); + } + + final regionToOutlineMatrix = _getRegionToOutlineMatrix(currentState); + final outlineToRegionMatrix = Matrix4.inverted(regionToOutlineMatrix); + + Rect pinnedRect(Rect Function(Size targetSize) forSize) { + final targetSize = sizeToKeepArea(); + final rect = forSize(targetSize); + + // do not try to coerce outline handled outside tilted image + if (transformation.straightenDegrees != 0) return rect; + + final regionCorners = { + rect.topLeft, + rect.topRight, + rect.bottomRight, + rect.bottomLeft, + }.map(outlineToRegionMatrix.transformOffset).toSet(); + + if (regionCorners.every((v) => contentRect.containsIncludingBottomRight(v, tolerance: precisionErrorTolerance))) return rect; + + final clampedOutlineCorners = regionCorners.map((v) => regionToOutlineMatrix.transformOffset(Offset(v.dx.clamp(0, contentSize.width), v.dy.clamp(0, contentSize.height)))).toSet(); + final minX = clampedOutlineCorners.map((v) => v.dx).min; + final maxX = clampedOutlineCorners.map((v) => v.dx).max; + final minY = clampedOutlineCorners.map((v) => v.dy).min; + final maxY = clampedOutlineCorners.map((v) => v.dy).max; + + var width = rect.width; + var height = rect.height; + if (rect.left < minX - precisionErrorTolerance) { + width = rect.right - minX; + height = width / newRatio; + } else if (rect.top < minY - precisionErrorTolerance) { + height = rect.bottom - minY; + width = height * newRatio; + } else if (rect.right > maxX + precisionErrorTolerance) { + width = maxX - rect.left; + height = width / newRatio; + } else if (rect.bottom > maxY + precisionErrorTolerance) { + height = maxY - rect.top; + width = height * newRatio; + } + final clampedSize = Size(width, height); + return clampedSize < targetSize ? forSize(clampedSize) : rect; + } + + switch (strategy) { + case _RatioStrategy.keepArea: + final targetSize = sizeToKeepArea(); + return Rect.fromCenter( + center: outline.center, + width: targetSize.width, + height: targetSize.height, + ); + case _RatioStrategy.contain: + final currentRatio = outline.width / outline.height; + if ((newRatio - currentRatio).abs() < precisionErrorTolerance) { + return outline; + } else { + late final Size targetSize; + if (newRatio > currentRatio) { + targetSize = Size(outline.width, outline.width / newRatio); + } else { + targetSize = Size(outline.height * newRatio, outline.height); + } + return Rect.fromCenter( + center: outline.center, + width: targetSize.width, + height: targetSize.height, + ); + } + case _RatioStrategy.pinTopLeft: + return pinnedRect((targetSize) => Rect.fromPoints( + outline.topLeft, + outline.topLeft.translate(targetSize.width, targetSize.height), + )); + case _RatioStrategy.pinTopRight: + return pinnedRect((targetSize) => Rect.fromPoints( + outline.topRight, + outline.topRight.translate(-targetSize.width, targetSize.height), + )); + case _RatioStrategy.pinBottomRight: + return pinnedRect((targetSize) => Rect.fromPoints( + outline.bottomRight, + outline.bottomRight.translate(-targetSize.width, -targetSize.height), + )); + case _RatioStrategy.pinBottomLeft: + return pinnedRect((targetSize) => Rect.fromPoints( + outline.bottomLeft, + outline.bottomLeft.translate(targetSize.width, -targetSize.height), + )); + case _RatioStrategy.pinLeft: + return pinnedRect((targetSize) => Rect.fromLTRB( + outline.left, + outline.center.dy - targetSize.height / 2, + outline.left + targetSize.width, + outline.center.dy + targetSize.height / 2, + )); + case _RatioStrategy.pinTop: + return pinnedRect((targetSize) => Rect.fromLTRB( + outline.center.dx - targetSize.width / 2, + outline.top, + outline.center.dx + targetSize.width / 2, + outline.top + targetSize.height, + )); + case _RatioStrategy.pinRight: + return pinnedRect((targetSize) => Rect.fromLTRB( + outline.right - targetSize.width, + outline.center.dy - targetSize.height / 2, + outline.right, + outline.center.dy + targetSize.height / 2, + )); + case _RatioStrategy.pinBottom: + return pinnedRect((targetSize) => Rect.fromLTRB( + outline.center.dx - targetSize.width / 2, + outline.bottom - targetSize.height, + outline.center.dx + targetSize.width / 2, + outline.bottom, + )); + } + } +} + +enum _RatioStrategy { keepArea, contain, pinTopLeft, pinTopRight, pinBottomRight, pinBottomLeft, pinLeft, pinTop, pinRight, pinBottom } diff --git a/lib/widgets/editor/transform/handles.dart b/lib/widgets/editor/transform/handles.dart new file mode 100644 index 000000000..9bcd87ad7 --- /dev/null +++ b/lib/widgets/editor/transform/handles.dart @@ -0,0 +1,120 @@ +import 'package:aves/widgets/editor/transform/cropper.dart'; +import 'package:flutter/material.dart'; + +class VertexHandle extends StatefulWidget { + final EdgeInsets padding; + final ValueGetter getPosition; + final ValueSetter setPosition; + final VoidCallback onDragStart, onDragEnd; + + const VertexHandle({ + super.key, + required this.padding, + required this.getPosition, + required this.setPosition, + required this.onDragStart, + required this.onDragEnd, + }); + + @override + State createState() => _VertexHandleState(); +} + +class _VertexHandleState extends State { + Offset _start = Offset.zero; + Offset _totalDelta = Offset.zero; + + static const double _handleDim = Cropper.handleDimension; + + EdgeInsets get padding => widget.padding; + + @override + Widget build(BuildContext context) { + return Positioned.fromRect( + rect: Rect.fromCenter( + center: widget.getPosition().translate(padding.left, padding.right), + width: _handleDim, + height: _handleDim, + ), + child: GestureDetector( + onPanStart: (details) { + _totalDelta = Offset.zero; + _start = widget.getPosition(); + widget.onDragStart(); + }, + onPanUpdate: (details) { + _totalDelta += details.delta; + widget.setPosition(_start + _totalDelta); + }, + onPanEnd: (details) { + widget.onDragEnd(); + }, + child: const ColoredBox( + color: Colors.transparent, + ), + ), + ); + } +} + +class EdgeHandle extends StatefulWidget { + final EdgeInsets padding; + final ValueGetter getEdge; + final ValueSetter setEdge; + final VoidCallback onDragStart, onDragEnd; + + const EdgeHandle({ + super.key, + required this.padding, + required this.getEdge, + required this.setEdge, + required this.onDragStart, + required this.onDragEnd, + }); + + @override + State createState() => _EdgeHandleState(); +} + +class _EdgeHandleState extends State { + Rect _start = Rect.zero; + Offset _totalDelta = Offset.zero; + + static const double _handleDim = Cropper.handleDimension; + + EdgeInsets get padding => widget.padding; + + @override + Widget build(BuildContext context) { + var edge = widget.getEdge(); + if (edge.width > _handleDim && edge.height == 0) { + // horizontal edge + edge = Rect.fromLTWH(edge.left + _handleDim / 2, edge.top - _handleDim / 2, edge.width - _handleDim, _handleDim); + } else if (edge.height > _handleDim && edge.width == 0) { + // vertical edge + edge = Rect.fromLTWH(edge.left - _handleDim / 2, edge.top + _handleDim / 2, _handleDim, edge.height - _handleDim); + } + edge = edge.translate(padding.left, padding.right); + + return Positioned.fromRect( + rect: edge, + child: GestureDetector( + onPanStart: (details) { + _totalDelta = Offset.zero; + _start = widget.getEdge(); + widget.onDragStart(); + }, + onPanUpdate: (details) { + _totalDelta += details.delta; + widget.setEdge(Rect.fromLTWH(_start.left + _totalDelta.dx, _start.top + _totalDelta.dy, _start.width, _start.height)); + }, + onPanEnd: (details) { + widget.onDragEnd(); + }, + child: const ColoredBox( + color: Colors.transparent, + ), + ), + ); + } +} diff --git a/lib/widgets/editor/transform/painter.dart b/lib/widgets/editor/transform/painter.dart new file mode 100644 index 000000000..90f82433c --- /dev/null +++ b/lib/widgets/editor/transform/painter.dart @@ -0,0 +1,131 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class CropperPainter extends CustomPainter { + final Rect rect; + final double gridOpacity; + final int gridDivision; + + const CropperPainter({ + required this.rect, + required this.gridOpacity, + required this.gridDivision, + }); + + static const double handleLength = kMinInteractiveDimension / 3 - 4; + static const double handleWidth = 3; + static const double borderWidth = 1; + static const double gridWidth = 1; + + static const cornerColor = Colors.white; + static final borderColor = Colors.white.withOpacity(.5); + static final gridColor = Colors.white.withOpacity(.5); + + @override + void paint(Canvas canvas, Size size) { + final cornerPaint = Paint() + ..style = PaintingStyle.fill + ..strokeCap = StrokeCap.round + ..strokeWidth = handleWidth + ..color = cornerColor; + final gridPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = gridWidth + ..color = gridColor.withOpacity(gridColor.opacity * gridOpacity); + + final xLeft = rect.left; + final yTop = rect.top; + final xRight = rect.right; + final yBottom = rect.bottom; + + final gridLeft = xLeft + borderWidth / 2; + final gridRight = xRight - borderWidth / 2; + final yStep = (yBottom - yTop) / gridDivision; + for (var i = 1; i < gridDivision; i++) { + canvas.drawLine( + Offset(gridLeft, yTop + i * yStep), + Offset(gridRight, yTop + i * yStep), + gridPaint, + ); + } + final gridTop = yTop + borderWidth / 2; + final gridBottom = yBottom - borderWidth / 2; + final xStep = (xRight - xLeft) / gridDivision; + for (var i = 1; i < gridDivision; i++) { + canvas.drawLine( + Offset(xLeft + i * xStep, gridTop), + Offset(xLeft + i * xStep, gridBottom), + gridPaint, + ); + } + + canvas.drawPoints( + PointMode.polygon, + [ + rect.topLeft.translate(0, handleLength), + rect.topLeft, + rect.topLeft.translate(handleLength, 0), + ], + cornerPaint); + + canvas.drawPoints( + PointMode.polygon, + [ + rect.topRight.translate(-handleLength, 0), + rect.topRight, + rect.topRight.translate(0, handleLength), + ], + cornerPaint); + + canvas.drawPoints( + PointMode.polygon, + [ + rect.bottomRight.translate(0, -handleLength), + rect.bottomRight, + rect.bottomRight.translate(-handleLength, 0), + ], + cornerPaint); + + canvas.drawPoints( + PointMode.polygon, + [ + rect.bottomLeft.translate(handleLength, 0), + rect.bottomLeft, + rect.bottomLeft.translate(0, -handleLength), + ], + cornerPaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} + +class ScrimPainter extends CustomPainter { + final Path excludePath; + final double opacity; + + const ScrimPainter({ + required this.excludePath, + required this.opacity, + }); + + static const double borderWidth = 1; + + static const scrimColor = Colors.black; + + @override + void paint(Canvas canvas, Size size) { + final scrimPaint = Paint() + ..style = PaintingStyle.fill + ..color = scrimColor.withOpacity(opacity); + + final outside = Path() + ..addRect(Rect.fromLTWH(0, 0, size.width, size.height).inflate(.5)) + ..close(); + canvas.drawPath(Path.combine(PathOperation.difference, outside, excludePath), scrimPaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/lib/widgets/editor/transform/transformation.dart b/lib/widgets/editor/transform/transformation.dart new file mode 100644 index 000000000..455b62cb6 --- /dev/null +++ b/lib/widgets/editor/transform/transformation.dart @@ -0,0 +1,87 @@ +import 'dart:math' as math; + +import 'package:aves/widgets/editor/transform/crop_region.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:vector_math/vector_math_64.dart'; + +@immutable +class Transformation extends Equatable { + final TransformOrientation orientation; + final double straightenDegrees; + final CropRegion region; + + @override + List get props => [orientation, straightenDegrees, region]; + + static const zero = Transformation( + orientation: TransformOrientation.normal, + straightenDegrees: 0, + region: CropRegion.zero, + ); + + const Transformation({ + required this.orientation, + required this.straightenDegrees, + required this.region, + }); + + Transformation copyWith({ + TransformOrientation? orientation, + double? straightenDegrees, + CropRegion? region, + }) { + return Transformation( + orientation: orientation ?? this.orientation, + straightenDegrees: straightenDegrees ?? this.straightenDegrees, + region: region ?? this.region, + ); + } + + Matrix4 get matrix => _orientationMatrix..multiply(_straightenMatrix); + + Matrix4 get _orientationMatrix { + final matrix = Matrix4.identity(); + switch (orientation) { + case TransformOrientation.normal: + break; + case TransformOrientation.rotate90: + matrix.rotateZ(math.pi / 2); + break; + case TransformOrientation.rotate180: + matrix.rotateZ(math.pi); + break; + case TransformOrientation.rotate270: + matrix.rotateZ(3 * math.pi / 2); + break; + case TransformOrientation.transverse: + matrix.scale(-1.0, 1.0, 1.0); + matrix.rotateZ(-3 * math.pi / 2); + break; + case TransformOrientation.flipVertical: + matrix.scale(1.0, -1.0, 1.0); + break; + case TransformOrientation.transpose: + matrix.scale(-1.0, 1.0, 1.0); + matrix.rotateZ(-1 * math.pi / 2); + break; + case TransformOrientation.flipHorizontal: + matrix.scale(-1.0, 1.0, 1.0); + break; + } + return matrix; + } + + Matrix4 get _straightenMatrix => Matrix4.rotationZ(degToRadian((orientation.isFlipped ? -1 : 1) * straightenDegrees)); +} + +@immutable +class TransformEvent { + final TransformActivity activity; + + const TransformEvent({ + required this.activity, + }); +} diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index 259fa0728..a40a79a66 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -123,7 +123,6 @@ class AlbumListPage extends StatelessWidget { if (sections.containsKey(vaultKey)) vaultKey: sections[vaultKey]!, if (sections.containsKey(regularKey)) regularKey: sections[regularKey]!, }; - break; case AlbumChipGroupFactor.mimeType: final visibleEntries = source.visibleEntries; sections = groupBy, ChipSectionKey>(unpinnedMapEntries, (kv) { @@ -134,12 +133,10 @@ class AlbumListPage extends StatelessWidget { if (!hasImage && hasVideo) return MimeTypeSectionKey.videos(context); return MimeTypeSectionKey.mixed(context); }); - break; case AlbumChipGroupFactor.volume: sections = groupBy, ChipSectionKey>(unpinnedMapEntries, (kv) { return StorageVolumeSectionKey(context, androidFileUtils.getStorageVolume(kv.filter.album)); }); - break; case AlbumChipGroupFactor.none: return { if (sortedMapEntries.isNotEmpty) diff --git a/lib/widgets/filter_grids/common/action_delegates/album_set.dart b/lib/widgets/filter_grids/common/action_delegates/album_set.dart index 7c46687a4..9a46e474d 100644 --- a/lib/widgets/filter_grids/common/action_delegates/album_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/album_set.dart @@ -147,25 +147,19 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with // general case ChipSetAction.createAlbum: _createAlbum(context, locked: false); - break; case ChipSetAction.createVault: _createAlbum(context, locked: true); - break; // single/multiple filters case ChipSetAction.delete: _delete(context, filters); - break; case ChipSetAction.lockVault: lockFilters(filters); browse(context); - break; // single filter case ChipSetAction.rename: _rename(context, filters.first); - break; case ChipSetAction.configureVault: _configureVault(context, filters.first); - break; default: break; } @@ -326,6 +320,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with ); if (confirmed == null || !confirmed) return; + settings.pinnedFilters = settings.pinnedFilters..removeAll(filters); source.forgetNewAlbums(todoAlbums); source.cleanEmptyAlbums(emptyAlbums); diff --git a/lib/widgets/filter_grids/common/action_delegates/chip.dart b/lib/widgets/filter_grids/common/action_delegates/chip.dart index b5fe5b64a..5d3760a43 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip.dart @@ -40,22 +40,16 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin { switch (action) { case ChipAction.goToAlbumPage: _goTo(context, filter, AlbumListPage.routeName, (context) => const AlbumListPage()); - break; case ChipAction.goToCountryPage: _goTo(context, filter, CountryListPage.routeName, (context) => const CountryListPage()); - break; case ChipAction.goToPlacePage: _goTo(context, filter, PlaceListPage.routeName, (context) => const PlaceListPage()); - break; case ChipAction.goToTagPage: _goTo(context, filter, TagListPage.routeName, (context) => const TagListPage()); - break; case ChipAction.reverse: ReverseFilterNotification(filter).dispatch(context); - break; case ChipAction.hide: _hide(context, filter); - break; case ChipAction.lockVault: if (filter is AlbumFilter) { lockFilters({filter}); diff --git a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart index 111dd9960..4f4a8b92d 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -164,48 +164,36 @@ abstract class ChipSetActionDelegate with FeedbackMi // general case ChipSetAction.configureView: configureView(context); - break; case ChipSetAction.select: context.read>>().select(); - break; case ChipSetAction.selectAll: context.read>>().addToSelection(allItems); - break; case ChipSetAction.selectNone: context.read>>().clearSelection(); - break; // browsing case ChipSetAction.search: _goToSearch(context); - break; case ChipSetAction.toggleTitleSearch: context.read().toggle(); - break; case ChipSetAction.createAlbum: case ChipSetAction.createVault: break; // browsing or selecting case ChipSetAction.map: _goToMap(context, filters); - break; case ChipSetAction.slideshow: _goToSlideshow(context, filters); - break; case ChipSetAction.stats: _goToStats(context, filters); - break; // selecting (single/multiple filters) case ChipSetAction.hide: _hide(context, filters); - break; case ChipSetAction.pin: settings.pinnedFilters = settings.pinnedFilters..addAll(filters); browse(context); - break; case ChipSetAction.unpin: settings.pinnedFilters = settings.pinnedFilters..removeAll(filters); browse(context); - break; case ChipSetAction.delete: case ChipSetAction.lockVault: case ChipSetAction.showCountryStates: @@ -213,7 +201,6 @@ abstract class ChipSetActionDelegate with FeedbackMi // selecting (single filter) case ChipSetAction.setCover: _setCover(context, filters.first); - break; case ChipSetAction.rename: case ChipSetAction.configureVault: break; diff --git a/lib/widgets/filter_grids/common/action_delegates/country_set.dart b/lib/widgets/filter_grids/common/action_delegates/country_set.dart index 0f3cc66dc..669bb67d0 100644 --- a/lib/widgets/filter_grids/common/action_delegates/country_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/country_set.dart @@ -87,7 +87,6 @@ class CountryChipSetActionDelegate extends ChipSetActionDelegate case ChipSetAction.showCountryStates: _showStates(context, filters); browse(context); - break; default: break; } diff --git a/lib/widgets/filter_grids/common/action_delegates/tag_set.dart b/lib/widgets/filter_grids/common/action_delegates/tag_set.dart index d669b6fab..6c9e2324d 100644 --- a/lib/widgets/filter_grids/common/action_delegates/tag_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/tag_set.dart @@ -70,7 +70,6 @@ class TagChipSetActionDelegate extends ChipSetActionDelegate { // single/multiple filters case ChipSetAction.delete: _delete(context, filters); - break; default: break; } diff --git a/lib/widgets/filter_grids/common/app_bar.dart b/lib/widgets/filter_grids/common/app_bar.dart index 7c752c228..435576e71 100644 --- a/lib/widgets/filter_grids/common/app_bar.dart +++ b/lib/widgets/filter_grids/common/app_bar.dart @@ -61,10 +61,8 @@ class FilterGridAppBar().enabled, isMenuItem: true, ); - break; default: child = MenuRow(text: action.getText(context), icon: action.getIcon()); - break; } return PopupMenuItem( @@ -180,7 +178,7 @@ class _FilterGridAppBarState().textScaleFactor; + final textScaleFactor = MediaQuery.textScaleFactorOf(context); double height = kToolbarHeight * textScaleFactor; if (settings.useTvLayout) { height += CaptionedButton.getTelevisionButtonHeight(context); diff --git a/lib/widgets/filter_grids/common/covered_filter_chip.dart b/lib/widgets/filter_grids/common/covered_filter_chip.dart index a2e066edf..d1fa7f60c 100644 --- a/lib/widgets/filter_grids/common/covered_filter_chip.dart +++ b/lib/widgets/filter_grids/common/covered_filter_chip.dart @@ -111,26 +111,22 @@ class CoveredFilterChip extends StatelessWidget { // album filters themselves do not change, but decoration derived from it does chipKey = ValueKey(appInventory.areAppNamesReadyNotifier.value); } + final textScaleFactor = MediaQuery.textScaleFactorOf(context); return AvesFilterChip( key: chipKey, filter: _filter, showText: showText, showGenericIcon: false, decoration: AvesFilterDecoration( - widget: Selector( - selector: (context, mq) => mq.textScaleFactor, - builder: (context, textScaleFactor, child) { - return Padding( - padding: EdgeInsets.only( - bottom: infoHeight( - extent: extent, - textScaleFactor: textScaleFactor, - showText: showText, - ), - ), - child: child, - ); - }, + radius: radius(extent), + widget: Padding( + padding: EdgeInsets.only( + bottom: infoHeight( + extent: extent, + textScaleFactor: textScaleFactor, + showText: showText, + ), + ), child: entry == null ? StreamBuilder?>( stream: covers.colorChangeStream.where((event) => event == null || event.contains(_filter)), @@ -159,9 +155,9 @@ class CoveredFilterChip extends StatelessWidget { : ThumbnailImage( entry: entry, extent: thumbnailExtent, + devicePixelRatio: MediaQuery.devicePixelRatioOf(context), ), ), - radius: radius(extent), ), banner: banner, details: showText ? _buildDetails(context, source, _filter) : null, diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 168e616f8..7daaca92a 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -29,7 +29,6 @@ import 'package:aves/widgets/common/grid/sliver.dart'; import 'package:aves/widgets/common/grid/theme.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart'; -import 'package:aves/widgets/common/providers/query_provider.dart'; import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart'; import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; @@ -82,38 +81,30 @@ class FilterGridPage extends StatelessWidget { @override Widget build(BuildContext context) { final useTvLayout = settings.useTvLayout; - final body = QueryProvider( - initialQuery: null, - child: GestureAreaProtectorStack( - child: DirectionalSafeArea( - start: !useTvLayout, - top: false, - bottom: false, - child: Selector( - selector: (context, mq) => mq.padding.top, - builder: (context, mqPaddingTop, child) { - return ValueListenableBuilder( - valueListenable: appBarHeightNotifier, - builder: (context, appBarHeight, child) { - return _FilterGrid( - // key is expected by test driver - key: const Key('filter-grid'), - settingsRouteKey: settingsRouteKey, - appBar: appBar, - appBarHeight: mqPaddingTop + appBarHeight, - sections: sections, - newFilters: newFilters, - sortFactor: sortFactor, - showHeaders: showHeaders, - selectable: selectable, - applyQuery: applyQuery, - emptyBuilder: emptyBuilder, - heroType: heroType, - ); - }, - ); - }, - ), + final body = GestureAreaProtectorStack( + child: DirectionalSafeArea( + start: !useTvLayout, + top: false, + bottom: false, + child: ValueListenableBuilder( + valueListenable: appBarHeightNotifier, + builder: (context, appBarHeight, child) { + return _FilterGrid( + // key is expected by test driver + key: const Key('filter-grid'), + settingsRouteKey: settingsRouteKey, + appBar: appBar, + appBarHeight: MediaQuery.paddingOf(context).top + appBarHeight, + sections: sections, + newFilters: newFilters, + sortFactor: sortFactor, + showHeaders: showHeaders, + selectable: selectable, + applyQuery: applyQuery, + emptyBuilder: emptyBuilder, + heroType: heroType, + ); + }, ), ), ); @@ -336,80 +327,76 @@ class _FilterGridContentState extends State<_FilterG // do not listen for animation delay change final target = context.read().staggeredAnimationPageTarget; final tileAnimationDelay = context.read().getTileAnimationDelay(target); - return Selector( - selector: (context, mq) => mq.textScaleFactor, - builder: (context, textScaleFactor, child) { - final tileHeight = CoveredFilterChip.tileHeight( - extent: thumbnailExtent, - textScaleFactor: textScaleFactor, - showText: tileLayout != TileLayout.list, - ); - return GridTheme( - extent: thumbnailExtent, - child: FilterListDetailsTheme( - extent: thumbnailExtent, - child: AnimatedBuilder( - animation: vaults, - builder: (context, child) { - return SectionedFilterListLayoutProvider( - sections: visibleSections, - showHeaders: widget.showHeaders, - selectable: widget.selectable, - tileLayout: tileLayout, - scrollableWidth: scrollableWidth, - columnCount: columnCount, - spacing: tileSpacing, - horizontalPadding: horizontalPadding, - tileWidth: thumbnailExtent, - tileHeight: tileHeight, - tileBuilder: (gridItem, tileSize) { - final extent = tileSize.shortestSide; - final tile = InteractiveFilterTile( - gridItem: gridItem, - chipExtent: extent, - thumbnailExtent: extent, - tileLayout: tileLayout, - banner: _getFilterBanner(context, gridItem.filter), - heroType: widget.heroType, - ); - if (!settings.useTvLayout) return tile; - return Focus( - onFocusChange: (focused) { - if (focused) { - _focusedItemNotifier.value = gridItem; - } else if (_focusedItemNotifier.value == gridItem) { - _focusedItemNotifier.value = null; - } - }, - child: ValueListenableBuilder?>( - valueListenable: _focusedItemNotifier, - builder: (context, focusedItem, child) { - return AnimatedScale( - scale: focusedItem == gridItem ? 1 : .9, - curve: Curves.fastOutSlowIn, - duration: context.select((v) => v.tvImageFocusAnimation), - child: child!, - ); - }, - child: tile, - ), - ); + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + final tileHeight = CoveredFilterChip.tileHeight( + extent: thumbnailExtent, + textScaleFactor: textScaleFactor, + showText: tileLayout != TileLayout.list, + ); + return GridTheme( + extent: thumbnailExtent, + child: FilterListDetailsTheme( + extent: thumbnailExtent, + child: AnimatedBuilder( + animation: vaults, + builder: (context, child) { + return SectionedFilterListLayoutProvider( + sections: visibleSections, + showHeaders: widget.showHeaders, + selectable: widget.selectable, + tileLayout: tileLayout, + scrollableWidth: scrollableWidth, + columnCount: columnCount, + spacing: tileSpacing, + horizontalPadding: horizontalPadding, + tileWidth: thumbnailExtent, + tileHeight: tileHeight, + tileBuilder: (gridItem, tileSize) { + final extent = tileSize.shortestSide; + final tile = InteractiveFilterTile( + gridItem: gridItem, + chipExtent: extent, + thumbnailExtent: extent, + tileLayout: tileLayout, + banner: _getFilterBanner(context, gridItem.filter), + heroType: widget.heroType, + ); + if (!settings.useTvLayout) return tile; + + return Focus( + onFocusChange: (focused) { + if (focused) { + _focusedItemNotifier.value = gridItem; + } else if (_focusedItemNotifier.value == gridItem) { + _focusedItemNotifier.value = null; + } }, - tileAnimationDelay: tileAnimationDelay, - coverRatioResolver: (item) { - final coverEntry = source.coverEntry(item.filter) ?? item.entry; - return coverEntry?.displayAspectRatio ?? 1; - }, - child: child!, + child: ValueListenableBuilder?>( + valueListenable: _focusedItemNotifier, + builder: (context, focusedItem, child) { + return AnimatedScale( + scale: focusedItem == gridItem ? 1 : .9, + curve: Curves.fastOutSlowIn, + duration: context.select((v) => v.tvImageFocusAnimation), + child: child!, + ); + }, + child: tile, + ), ); }, - child: child, - ), - ), - ); - }, - child: child, + tileAnimationDelay: tileAnimationDelay, + coverRatioResolver: (item) { + final coverEntry = source.coverEntry(item.filter) ?? item.entry; + return coverEntry?.displayAspectRatio ?? 1; + }, + child: child!, + ); + }, + child: child, + ), + ), ); }, child: child, @@ -583,7 +570,7 @@ class _FilterScaler extends StatelessWidget { @override Widget build(BuildContext context) { - final textScaleFactor = context.select((mq) => mq.textScaleFactor); + final textScaleFactor = MediaQuery.textScaleFactorOf(context); final metrics = context.select>((v) => Tuple2(v.spacing, v.horizontalPadding)); final tileSpacing = metrics.item1; final horizontalPadding = metrics.item2; diff --git a/lib/widgets/filter_grids/common/filter_nav_page.dart b/lib/widgets/filter_grids/common/filter_nav_page.dart index 7f235c29f..864bef00d 100644 --- a/lib/widgets/filter_grids/common/filter_nav_page.dart +++ b/lib/widgets/filter_grids/common/filter_nav_page.dart @@ -2,6 +2,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/utils/time_utils.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:aves/widgets/common/providers/query_provider.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; import 'package:aves/widgets/filter_grids/common/app_bar.dart'; @@ -75,22 +76,18 @@ class FilterNavigationPage MapEntry(filter, source.count(filter)))); filtersWithCount.sort(compareFiltersByEntryCount); filters = filtersWithCount.map((kv) => kv.key).toSet(); allMapEntries = toGridItem(source, filters); - break; case ChipSortFactor.size: final filtersWithSize = List.of(filters.map((filter) => MapEntry(filter, source.size(filter)))); filtersWithSize.sort(compareFiltersBySize); filters = filtersWithSize.map((kv) => kv.key).toSet(); allMapEntries = toGridItem(source, filters); - break; } if (reverse) { allMapEntries = allMapEntries.reversed.toList(); @@ -106,30 +103,32 @@ class _FilterNavigationPageState>( child: Builder( - builder: (context) => FilterGridPage( - appBar: FilterGridAppBar( - source: widget.source, - title: widget.title, - actionDelegate: widget.actionDelegate, - isEmpty: widget.filterSections.isEmpty, + builder: (context) => QueryProvider( + child: FilterGridPage( + appBar: FilterGridAppBar( + source: widget.source, + title: widget.title, + actionDelegate: widget.actionDelegate, + isEmpty: widget.filterSections.isEmpty, + appBarHeightNotifier: _appBarHeightNotifier, + ), appBarHeightNotifier: _appBarHeightNotifier, + sections: widget.filterSections, + newFilters: widget.newFilters ?? {}, + sortFactor: widget.sortFactor, + showHeaders: widget.showHeaders, + selectable: true, + applyQuery: widget.applyQuery, + emptyBuilder: () => ValueListenableBuilder( + valueListenable: widget.source.stateNotifier, + builder: (context, sourceState, child) { + return sourceState != SourceState.loading ? widget.emptyBuilder() : const SizedBox(); + }, + ), + // do not always enable hero, otherwise unwanted hero gets triggered + // when using `Show in [...]` action from a chip in the Collection filter bar + heroType: HeroType.onTap, ), - appBarHeightNotifier: _appBarHeightNotifier, - sections: widget.filterSections, - newFilters: widget.newFilters ?? {}, - sortFactor: widget.sortFactor, - showHeaders: widget.showHeaders, - selectable: true, - applyQuery: widget.applyQuery, - emptyBuilder: () => ValueListenableBuilder( - valueListenable: widget.source.stateNotifier, - builder: (context, sourceState, child) { - return sourceState != SourceState.loading ? widget.emptyBuilder() : const SizedBox(); - }, - ), - // do not always enable hero, otherwise unwanted hero gets triggered - // when using `Show in [...]` action from a chip in the Collection filter bar - heroType: HeroType.onTap, ), ), ); diff --git a/lib/widgets/filter_grids/common/filter_tile.dart b/lib/widgets/filter_grids/common/filter_tile.dart index 4ac473b8c..fbb72546e 100644 --- a/lib/widgets/filter_grids/common/filter_tile.dart +++ b/lib/widgets/filter_grids/common/filter_tile.dart @@ -65,15 +65,14 @@ class _InteractiveFilterTileState extends State(filter); - break; case AppMode.pickMediaInternal: case AppMode.screenSaver: case AppMode.setWallpaper: case AppMode.slideshow: case AppMode.view: + case AppMode.edit: case null: break; } diff --git a/lib/widgets/filter_grids/common/list_details.dart b/lib/widgets/filter_grids/common/list_details.dart index 37e6224c4..872fef703 100644 --- a/lib/widgets/filter_grids/common/list_details.dart +++ b/lib/widgets/filter_grids/common/list_details.dart @@ -86,7 +86,7 @@ class FilterListDetails extends StatelessWidget { Widget _buildDateRow(BuildContext context, FilterListDetailsThemeData detailsTheme, bool hasTitleLeading) { final locale = context.l10n.localeName; - final use24hour = context.select((v) => v.alwaysUse24HourFormat); + final use24hour = MediaQuery.alwaysUse24HourFormatOf(context); final date = entry?.bestDate; final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable; diff --git a/lib/widgets/filter_grids/common/query_bar.dart b/lib/widgets/filter_grids/common/query_bar.dart index 4f861b861..a6a09228b 100644 --- a/lib/widgets/filter_grids/common/query_bar.dart +++ b/lib/widgets/filter_grids/common/query_bar.dart @@ -17,7 +17,7 @@ class FilterQueryBar extends StatelessWidget { @override Widget build(BuildContext context) { - final textScaleFactor = context.select((mq) => mq.textScaleFactor); + final textScaleFactor = MediaQuery.textScaleFactorOf(context); return Container( height: getPreferredHeight(textScaleFactor), alignment: Alignment.topCenter, diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index ff4defde4..5f55824e1 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -23,7 +23,10 @@ import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/search/page.dart'; import 'package:aves/widgets/common/search/route.dart'; +import 'package:aves/widgets/editor/entry_editor_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; +import 'package:aves/widgets/filter_grids/tags_page.dart'; +import 'package:aves/widgets/intent.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/settings/home_widget_settings_page.dart'; import 'package:aves/widgets/settings/screen_saver_settings_page.dart'; @@ -57,26 +60,6 @@ class _HomePageState extends State { String? _initialRouteName, _initialSearchQuery; Set? _initialFilters; - static const actionPickItems = 'pick_items'; - static const actionPickCollectionFilters = 'pick_collection_filters'; - static const actionScreenSaver = 'screen_saver'; - static const actionScreenSaverSettings = 'screen_saver_settings'; - static const actionSearch = 'search'; - 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 intentDataKeySafeMode = 'safeMode'; - static const intentDataKeyUri = 'uri'; - static const intentDataKeyWidgetId = 'widgetId'; - static const allowedShortcutRoutes = [ CollectionPage.routeName, AlbumListPage.routeName, @@ -103,40 +86,44 @@ class _HomePageState extends State { var appMode = AppMode.main; final intentData = widget.intentData ?? await IntentService.getIntentData(); - final safeMode = intentData[intentDataKeySafeMode] ?? false; - final intentAction = intentData[intentDataKeyAction]; + final safeMode = intentData[IntentDataKeys.safeMode] ?? false; + final intentAction = intentData[IntentDataKeys.action]; _initialFilters = null; await androidFileUtils.init(); - if (!{actionScreenSaver, actionSetWallpaper}.contains(intentAction) && settings.isInstalledAppAccessAllowed) { + if (!{ + IntentActions.edit, + IntentActions.screenSaver, + IntentActions.setWallpaper, + }.contains(intentAction) && + settings.isInstalledAppAccessAllowed) { unawaited(appInventory.initAppNames()); } if (intentData.isNotEmpty) { await reportService.log('Intent data=$intentData'); switch (intentAction) { - case actionView: - case actionWidgetOpen: + case IntentActions.view: + case IntentActions.widgetOpen: String? uri, mimeType; - final widgetId = intentData[intentDataKeyWidgetId]; + final widgetId = intentData[IntentDataKeys.widgetId]; if (widgetId != null) { // widget settings may be modified in a different process after channel setup await settings.reload(); final page = settings.getWidgetOpenPage(widgetId); switch (page) { case WidgetOpenPage.home: + case WidgetOpenPage.updateWidget: break; case WidgetOpenPage.collection: _initialFilters = settings.getWidgetCollectionFilters(widgetId); - break; case WidgetOpenPage.viewer: uri = settings.getWidgetUri(widgetId); - break; } unawaited(WidgetService.update(widgetId)); } else { - uri = intentData[intentDataKeyUri]; - mimeType = intentData[intentDataKeyMimeType]; + uri = intentData[IntentDataKeys.uri]; + mimeType = intentData[IntentDataKeys.mimeType]; } if (uri != null) { _viewerEntry = await _initViewerEntry( @@ -147,49 +134,51 @@ class _HomePageState extends State { appMode = AppMode.view; } } - break; - case actionPickItems: + case IntentActions.edit: + _viewerEntry = await _initViewerEntry( + uri: intentData[IntentDataKeys.uri], + mimeType: intentData[IntentDataKeys.mimeType], + ); + if (_viewerEntry != null) { + appMode = AppMode.edit; + } + case IntentActions.setWallpaper: + _viewerEntry = await _initViewerEntry( + uri: intentData[IntentDataKeys.uri], + mimeType: intentData[IntentDataKeys.mimeType], + ); + if (_viewerEntry != null) { + appMode = AppMode.setWallpaper; + } + case IntentActions.pickItems: // TODO TLAD apply pick mimetype(s) // some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?) - String? pickMimeTypes = intentData[intentDataKeyMimeType]; - final multiple = intentData[intentDataKeyAllowMultiple] ?? false; + String? pickMimeTypes = intentData[IntentDataKeys.mimeType]; + final multiple = intentData[IntentDataKeys.allowMultiple] ?? false; debugPrint('pick mimeType=$pickMimeTypes multiple=$multiple'); appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal; - break; - case actionPickCollectionFilters: + case IntentActions.pickCollectionFilters: appMode = AppMode.pickCollectionFiltersExternal; - break; - case actionScreenSaver: + case IntentActions.screenSaver: appMode = AppMode.screenSaver; _initialRouteName = ScreenSaverPage.routeName; - break; - case actionScreenSaverSettings: + case IntentActions.screenSaverSettings: _initialRouteName = ScreenSaverSettingsPage.routeName; - break; - case actionSearch: + case IntentActions.search: _initialRouteName = SearchPage.routeName; - _initialSearchQuery = intentData[intentDataKeyQuery]; - break; - case actionSetWallpaper: - appMode = AppMode.setWallpaper; - _viewerEntry = await _initViewerEntry( - uri: intentData[intentDataKeyUri], - mimeType: intentData[intentDataKeyMimeType], - ); - break; - case actionWidgetSettings: + _initialSearchQuery = intentData[IntentDataKeys.query]; + case IntentActions.widgetSettings: _initialRouteName = HomeWidgetSettingsPage.routeName; - _widgetId = intentData[intentDataKeyWidgetId] ?? 0; - break; + _widgetId = intentData[IntentDataKeys.widgetId] ?? 0; default: // do not use 'route' as extra key, as the Flutter framework acts on it - final extraRoute = intentData[intentDataKeyPage]; + final extraRoute = intentData[IntentDataKeys.page]; if (allowedShortcutRoutes.contains(extraRoute)) { _initialRouteName = extraRoute; } } if (_initialFilters == null) { - final extraFilters = intentData[intentDataKeyFilters]; + final extraFilters = intentData[IntentDataKeys.filters]; _initialFilters = extraFilters != null ? (extraFilters as List).cast().map(CollectionFilter.fromJson).whereNotNull().toSet() : null; } } @@ -209,13 +198,11 @@ class _HomePageState extends State { loadTopEntriesFirst: settings.homePage == HomePageSetting.collection, canAnalyze: !safeMode, ); - break; case AppMode.screenSaver: final source = context.read(); await source.init( canAnalyze: false, ); - break; case AppMode.view: if (_isViewerSourceable(_viewerEntry)) { final directory = _viewerEntry?.directory; @@ -230,10 +217,9 @@ class _HomePageState extends State { } else { await _initViewerEssentials(); } - break; + case AppMode.edit: case AppMode.setWallpaper: await _initViewerEssentials(); - break; case AppMode.pickMediaInternal: case AppMode.pickFilterInternal: case AppMode.slideshow: @@ -271,106 +257,105 @@ class _HomePageState extends State { } Future _getRedirectRoute(AppMode appMode) async { - if (appMode == AppMode.setWallpaper) { - return DirectMaterialPageRoute( - settings: const RouteSettings(name: WallpaperPage.routeName), - builder: (_) { - return WallpaperPage( - entry: _viewerEntry, - ); - }, - ); - } - - if (appMode == AppMode.view) { - AvesEntry viewerEntry = _viewerEntry!; - CollectionLens? collection; - - final source = context.read(); - if (source.initState != SourceInitializationState.none) { - final album = viewerEntry.directory; - if (album != null) { - // wait for collection to pass the `loading` state - final completer = Completer(); - void _onSourceStateChanged() { - if (source.state != SourceState.loading) { - source.stateNotifier.removeListener(_onSourceStateChanged); - completer.complete(); - } - } - - source.stateNotifier.addListener(_onSourceStateChanged); - await completer.future; - - collection = CollectionLens( - source: source, - filters: {AlbumFilter(album, source.getAlbumDisplayName(context, album))}, - listenToSource: false, - // if we group bursts, opening a burst sub-entry should: - // - identify and select the containing main entry, - // - select the sub-entry in the Viewer page. - groupBursts: false, - ); - final viewerEntryPath = viewerEntry.path; - final collectionEntry = collection.sortedEntries.firstWhereOrNull((entry) => entry.path == viewerEntryPath); - if (collectionEntry != null) { - viewerEntry = collectionEntry; - } else { - debugPrint('collection does not contain viewerEntry=$viewerEntry'); - collection = null; - } - } - } - - return DirectMaterialPageRoute( - settings: const RouteSettings(name: EntryViewerPage.routeName), - builder: (_) { - return EntryViewerPage( - collection: collection, - initialEntry: viewerEntry, - ); - }, - ); - } - String routeName; Set? filters; switch (appMode) { case AppMode.pickSingleMediaExternal: case AppMode.pickMultipleMediaExternal: routeName = CollectionPage.routeName; - break; - default: + case AppMode.setWallpaper: + return DirectMaterialPageRoute( + settings: const RouteSettings(name: WallpaperPage.routeName), + builder: (_) { + return WallpaperPage( + entry: _viewerEntry, + ); + }, + ); + case AppMode.view: + AvesEntry viewerEntry = _viewerEntry!; + CollectionLens? collection; + + final source = context.read(); + if (source.initState != SourceInitializationState.none) { + final album = viewerEntry.directory; + if (album != null) { + // wait for collection to pass the `loading` state + final completer = Completer(); + void _onSourceStateChanged() { + if (source.state != SourceState.loading) { + source.stateNotifier.removeListener(_onSourceStateChanged); + completer.complete(); + } + } + + source.stateNotifier.addListener(_onSourceStateChanged); + await completer.future; + + collection = CollectionLens( + source: source, + filters: {AlbumFilter(album, source.getAlbumDisplayName(context, album))}, + listenToSource: false, + // if we group bursts, opening a burst sub-entry should: + // - identify and select the containing main entry, + // - select the sub-entry in the Viewer page. + groupBursts: false, + ); + final viewerEntryPath = viewerEntry.path; + final collectionEntry = collection.sortedEntries.firstWhereOrNull((entry) => entry.path == viewerEntryPath); + if (collectionEntry != null) { + viewerEntry = collectionEntry; + } else { + debugPrint('collection does not contain viewerEntry=$viewerEntry'); + collection = null; + } + } + } + + return DirectMaterialPageRoute( + settings: const RouteSettings(name: EntryViewerPage.routeName), + builder: (_) { + return EntryViewerPage( + collection: collection, + initialEntry: viewerEntry, + ); + }, + ); + case AppMode.edit: + return DirectMaterialPageRoute( + settings: const RouteSettings(name: EntryViewerPage.routeName), + builder: (_) { + return ImageEditorPage( + entry: _viewerEntry!, + ); + }, + ); + case AppMode.main: + case AppMode.pickCollectionFiltersExternal: + case AppMode.pickMediaInternal: + case AppMode.pickFilterInternal: + case AppMode.screenSaver: + case AppMode.slideshow: routeName = _initialRouteName ?? settings.homePage.routeName; filters = _initialFilters ?? {}; - break; } + Route buildRoute(WidgetBuilder builder) => DirectMaterialPageRoute( + settings: RouteSettings(name: routeName), + builder: builder, + ); + final source = context.read(); switch (routeName) { case AlbumListPage.routeName: - return DirectMaterialPageRoute( - settings: RouteSettings(name: routeName), - builder: (context) => const AlbumListPage(), - ); + return buildRoute((context) => const AlbumListPage()); + case TagListPage.routeName: + return buildRoute((context) => const TagListPage()); case ScreenSaverPage.routeName: - return DirectMaterialPageRoute( - settings: RouteSettings(name: routeName), - builder: (context) => ScreenSaverPage( - source: source, - ), - ); + return buildRoute((context) => ScreenSaverPage(source: source)); case ScreenSaverSettingsPage.routeName: - return DirectMaterialPageRoute( - settings: RouteSettings(name: routeName), - builder: (context) => const ScreenSaverSettingsPage(), - ); + return buildRoute((context) => const ScreenSaverSettingsPage()); case HomeWidgetSettingsPage.routeName: - return DirectMaterialPageRoute( - settings: RouteSettings(name: routeName), - builder: (context) => HomeWidgetSettingsPage( - widgetId: _widgetId!, - ), - ); + return buildRoute((context) => HomeWidgetSettingsPage(widgetId: _widgetId!)); case SearchPage.routeName: return SearchPageRoute( delegate: CollectionSearchDelegate( @@ -382,13 +367,7 @@ class _HomePageState extends State { ); case CollectionPage.routeName: default: - return DirectMaterialPageRoute( - settings: RouteSettings(name: routeName), - builder: (context) => CollectionPage( - source: source, - filters: filters, - ), - ); + return buildRoute((context) => CollectionPage(source: source, filters: filters)); } } } diff --git a/lib/widgets/home_widget.dart b/lib/widgets/home_widget.dart index 1e81d6e77..f01bce2ce 100644 --- a/lib/widgets/home_widget.dart +++ b/lib/widgets/home_widget.dart @@ -42,7 +42,7 @@ class HomeWidgetPainter { } final recorder = ui.PictureRecorder(); - final rect = Rect.fromLTWH(0, 0, widgetSizePx.width, widgetSizePx.height); + final rect = Offset.zero & widgetSizePx; final canvas = Canvas(recorder, rect); final path = shape.path(widgetSizePx, devicePixelRatio); canvas.clipPath(path); diff --git a/lib/widgets/intent.dart b/lib/widgets/intent.dart new file mode 100644 index 000000000..639cd5526 --- /dev/null +++ b/lib/widgets/intent.dart @@ -0,0 +1,24 @@ +class IntentActions { + static const edit = 'edit'; + static const pickItems = 'pick_items'; + static const pickCollectionFilters = 'pick_collection_filters'; + static const screenSaver = 'screen_saver'; + static const screenSaverSettings = 'screen_saver_settings'; + static const search = 'search'; + static const setWallpaper = 'set_wallpaper'; + static const view = 'view'; + static const widgetOpen = 'widget_open'; + static const widgetSettings = 'widget_settings'; +} + +class IntentDataKeys { + static const action = 'action'; + static const allowMultiple = 'allowMultiple'; + static const filters = 'filters'; + static const mimeType = 'mimeType'; + static const page = 'page'; + static const query = 'query'; + static const safeMode = 'safeMode'; + static const uri = 'uri'; + static const widgetId = 'widgetId'; +} diff --git a/lib/widgets/map/address_row.dart b/lib/widgets/map/address_row.dart index 407e24aab..daeab20ed 100644 --- a/lib/widgets/map/address_row.dart +++ b/lib/widgets/map/address_row.dart @@ -9,7 +9,6 @@ import 'package:aves/theme/styles.dart'; import 'package:aves/theme/text.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'info_row.dart'; @@ -44,12 +43,13 @@ class _MapAddressRowState extends State { @override Widget build(BuildContext context) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); return Container( alignment: AlignmentDirectional.centerStart, // addresses can include non-latin scripts with inconsistent line height, // which is especially an issue for relayout/painting of heavy Google map, // so we give extra height to give breathing room to the text and stabilize layout - height: Theme.of(context).textTheme.bodyMedium!.fontSize! * context.select((mq) => mq.textScaleFactor) * 2, + height: Theme.of(context).textTheme.bodyMedium!.fontSize! * textScaleFactor * 2, child: ValueListenableBuilder( valueListenable: _addressLineNotifier, builder: (context, addressLine, child) { diff --git a/lib/widgets/map/date_row.dart b/lib/widgets/map/date_row.dart index 430a731fc..33e5893ca 100644 --- a/lib/widgets/map/date_row.dart +++ b/lib/widgets/map/date_row.dart @@ -6,7 +6,6 @@ import 'package:aves/theme/text.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/map/info_row.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class MapDateRow extends StatelessWidget { final AvesEntry? entry; @@ -19,7 +18,7 @@ class MapDateRow extends StatelessWidget { @override Widget build(BuildContext context) { final locale = context.l10n.localeName; - final use24hour = context.select((v) => v.alwaysUse24HourFormat); + final use24hour = MediaQuery.alwaysUse24HourFormatOf(context); final date = entry?.bestDate; final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable; diff --git a/lib/widgets/map/info_row.dart b/lib/widgets/map/info_row.dart index fc58e1387..2bbb4eb25 100644 --- a/lib/widgets/map/info_row.dart +++ b/lib/widgets/map/info_row.dart @@ -3,7 +3,6 @@ import 'package:aves/widgets/map/address_row.dart'; import 'package:aves/widgets/map/date_row.dart'; import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class MapInfoRow extends StatelessWidget { final ValueNotifier entryNotifier; @@ -18,12 +17,11 @@ class MapInfoRow extends StatelessWidget { @override Widget build(BuildContext context) { - final orientation = context.select((v) => v.orientation); - return ValueListenableBuilder( valueListenable: entryNotifier, builder: (context, entry, child) { - final content = orientation == Orientation.portrait + final isPortrait = MediaQuery.orientationOf(context) == Orientation.portrait; + final content = isPortrait ? [ Expanded( child: Column( @@ -59,5 +57,8 @@ class MapInfoRow extends StatelessWidget { ); } - static double getIconSize(BuildContext context) => 16.0 * context.select((mq) => mq.textScaleFactor); + static double getIconSize(BuildContext context) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + return 16 * textScaleFactor; + } } diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 63a463d09..665ccc8ca 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -247,6 +247,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin controller: _mapController, collectionListenable: openingCollection, entries: openingCollection.sortedEntries, + availableSize: MediaQuery.sizeOf(context), initialCenter: widget.initialEntry?.latLng ?? widget.overlayEntry?.center, isAnimatingNotifier: _isPageAnimatingNotifier, dotLocationNotifier: _dotLocationNotifier, @@ -487,10 +488,8 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin } _mapController.moveTo(location); } - break; case MapClusterAction.removeLocation: await delegate.removeLocation(context, clusterEntries); - break; } } } diff --git a/lib/widgets/map/scroller.dart b/lib/widgets/map/scroller.dart index 54c9a1c6b..0d013ae12 100644 --- a/lib/widgets/map/scroller.dart +++ b/lib/widgets/map/scroller.dart @@ -7,7 +7,6 @@ import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/thumbnail/scroller.dart'; import 'package:aves/widgets/map/info_row.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class MapEntryScroller extends StatefulWidget { final ValueNotifier regionCollectionNotifier; @@ -35,6 +34,7 @@ class _MapEntryScrollerState extends State { void initState() { super.initState(); _registerWidget(widget); + WidgetsBinding.instance.addPostFrameCallback((_) => _onSelectedEntryChanged()); } @override @@ -71,30 +71,27 @@ class _MapEntryScrollerState extends State { child: MapInfoRow(entryNotifier: _infoEntryNotifier), ), const SizedBox(height: 8), - Selector( - selector: (context, mq) => mq.size.width, - builder: (context, mqWidth, child) => ValueListenableBuilder( - valueListenable: widget.regionCollectionNotifier, - builder: (context, regionCollection, child) { - return AnimatedBuilder( - // update when entries are added/removed - animation: regionCollection ?? ChangeNotifier(), - builder: (context, child) { - final regionEntries = regionCollection?.sortedEntries ?? []; - return ThumbnailScroller( - availableWidth: mqWidth, - entryCount: regionEntries.length, - entryBuilder: (index) => index < regionEntries.length ? regionEntries[index] : null, - indexNotifier: widget.selectedIndexNotifier, - onTap: widget.onTap, - heroTagger: (entry) => Object.hashAll([regionCollection?.id, entry.id]), - highlightable: true, - showLocation: false, - ); - }, - ); - }, - ), + ValueListenableBuilder( + valueListenable: widget.regionCollectionNotifier, + builder: (context, regionCollection, child) { + return AnimatedBuilder( + // update when entries are added/removed + animation: regionCollection ?? ChangeNotifier(), + builder: (context, child) { + final regionEntries = regionCollection?.sortedEntries ?? []; + return ThumbnailScroller( + availableWidth: MediaQuery.sizeOf(context).width, + entryCount: regionEntries.length, + entryBuilder: (index) => index < regionEntries.length ? regionEntries[index] : null, + indexNotifier: widget.selectedIndexNotifier, + onTap: widget.onTap, + heroTagger: (entry) => Object.hashAll([regionCollection?.id, entry.id]), + highlightable: true, + showLocation: false, + ); + }, + ); + }, ), ], ), diff --git a/lib/widgets/navigation/drawer/app_drawer.dart b/lib/widgets/navigation/drawer/app_drawer.dart index 8bb449524..37ac737cf 100644 --- a/lib/widgets/navigation/drawer/app_drawer.dart +++ b/lib/widgets/navigation/drawer/app_drawer.dart @@ -85,6 +85,7 @@ class _AppDrawerState extends State { child: Selector( selector: (context, mq) => mq.effectiveBottomPadding, builder: (context, mqPaddingBottom, child) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); final iconTheme = IconTheme.of(context); return SingleChildScrollView( controller: _scrollController, @@ -93,7 +94,7 @@ class _AppDrawerState extends State { padding: EdgeInsets.only(bottom: mqPaddingBottom), child: IconTheme( data: iconTheme.copyWith( - size: iconTheme.size! * MediaQuery.textScaleFactorOf(context), + size: iconTheme.size! * textScaleFactor, ), child: Column( children: drawerItems, @@ -238,25 +239,21 @@ class _AppDrawerState extends State { stream: source.eventBus.on(), builder: (context, _) => Text('${source.rawAlbums.length}'), ); - break; case CountryListPage.routeName: trailing = StreamBuilder( stream: source.eventBus.on(), builder: (context, _) => Text('${source.sortedCountries.length}'), ); - break; case PlaceListPage.routeName: trailing = StreamBuilder( stream: source.eventBus.on(), builder: (context, _) => Text('${source.sortedPlaces.length}'), ); - break; case TagListPage.routeName: trailing = StreamBuilder( stream: source.eventBus.on(), builder: (context, _) => Text('${source.sortedTags.length}'), ); - break; } return PageNavTile( diff --git a/lib/widgets/navigation/nav_bar/floating.dart b/lib/widgets/navigation/nav_bar/floating.dart index 6ebb4bfdd..85d729eb3 100644 --- a/lib/widgets/navigation/nav_bar/floating.dart +++ b/lib/widgets/navigation/nav_bar/floating.dart @@ -113,10 +113,8 @@ class _FloatingNavBarState extends State with SingleTickerProvid switch (event) { case DraggableScrollbarEvent.dragStart: _isDragging = true; - break; case DraggableScrollbarEvent.dragEnd: _isDragging = false; - break; } } } diff --git a/lib/widgets/navigation/tv_rail.dart b/lib/widgets/navigation/tv_rail.dart index a408e2a50..e1ef726d5 100644 --- a/lib/widgets/navigation/tv_rail.dart +++ b/lib/widgets/navigation/tv_rail.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/settings/enums/home_page.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -12,7 +13,6 @@ import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/debug/app_debug_page.dart'; -import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/navigation/drawer/app_drawer.dart'; import 'package:aves/widgets/navigation/drawer/page_nav_tile.dart'; import 'package:aves/widgets/navigation/drawer/tile.dart'; @@ -240,13 +240,16 @@ class _TvRailState extends State { return pageBookmarks.map(_routeNavEntry).toList(); } - _NavEntry _routeNavEntry(String routeName) => _NavEntry( - icon: DrawerPageIcon(route: routeName), - label: DrawerPageTitle(route: routeName), - isHome: settings.homePage == HomePageSetting.albums && routeName == AlbumListPage.routeName, - isSelected: context.currentRouteName == routeName, - onSelection: () => _goTo(routeName), - ); + _NavEntry _routeNavEntry(String routeName) { + final homePage = settings.homePage; + return _NavEntry( + icon: DrawerPageIcon(route: routeName), + label: DrawerPageTitle(route: routeName), + isHome: homePage != HomePageSetting.collection && routeName == homePage.routeName, + isSelected: context.currentRouteName == routeName, + onSelection: () => _goTo(routeName), + ); + } void _goTo(String routeName) { Navigator.maybeOf(context)?.pushAndRemoveUntil( diff --git a/lib/widgets/settings/app_export/items.dart b/lib/widgets/settings/app_export/items.dart index 3a94614b4..5ee21faa5 100644 --- a/lib/widgets/settings/app_export/items.dart +++ b/lib/widgets/settings/app_export/items.dart @@ -34,13 +34,10 @@ extension ExtraAppExportItem on AppExportItem { switch (this) { case AppExportItem.covers: covers.import(jsonMap, source); - break; case AppExportItem.favourites: favourites.import(jsonMap, source); - break; case AppExportItem.settings: await settings.import(jsonMap); - break; } } } diff --git a/lib/widgets/settings/common/quick_actions/editor_page.dart b/lib/widgets/settings/common/quick_actions/editor_page.dart index 0bec2c037..4ba51136c 100644 --- a/lib/widgets/settings/common/quick_actions/editor_page.dart +++ b/lib/widgets/settings/common/quick_actions/editor_page.dart @@ -317,13 +317,10 @@ class _QuickActionEditorBodyState extends State { case _PickerAction.toggleHiddenView: settings.filePickerShowHiddenFiles = !showHidden; setState(() {}); - break; } }, ), diff --git a/lib/widgets/settings/settings_mobile_page.dart b/lib/widgets/settings/settings_mobile_page.dart index 2950b0148..354e613f0 100644 --- a/lib/widgets/settings/settings_mobile_page.dart +++ b/lib/widgets/settings/settings_mobile_page.dart @@ -124,7 +124,6 @@ class _SettingsMobilePageState extends State with FeedbackMi showFeedback(context, context.l10n.genericFailureFeedback); } } - break; case SettingsAction.import: // specifying the JSON MIME type to restrict openable files is correct in theory, // but older devices (e.g. SM-P580, API 27) that do not recognize JSON files as such would filter them out @@ -172,7 +171,6 @@ class _SettingsMobilePageState extends State with FeedbackMi showFeedback(context, context.l10n.genericFailureFeedback); } } - break; } } diff --git a/lib/widgets/settings/thumbnails/overlay.dart b/lib/widgets/settings/thumbnails/overlay.dart index 3f8be2e1d..d136ddbe1 100644 --- a/lib/widgets/settings/thumbnails/overlay.dart +++ b/lib/widgets/settings/thumbnails/overlay.dart @@ -89,7 +89,10 @@ class ThumbnailOverlayPage extends StatelessWidget { ); } - static double _getIconSize(BuildContext context) => IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context); + static double _getIconSize(BuildContext context) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + return IconTheme.of(context).size! * textScaleFactor; + } static Color _getIconColor(BuildContext context) => context.select((v) => v.neutral); diff --git a/lib/widgets/stats/date/histogram.dart b/lib/widgets/stats/date/histogram.dart index 772c07310..69acdcc41 100644 --- a/lib/widgets/stats/date/histogram.dart +++ b/lib/widgets/stats/date/histogram.dart @@ -68,13 +68,10 @@ class _HistogramState extends State with AutomaticKeepAliveClientMixi switch (_level) { case DateLevel.ymd: normalizeDate = (v) => DateTime(v.year, v.month, v.day); - break; case DateLevel.ym: normalizeDate = (v) => DateTime(v.year, v.month); - break; default: normalizeDate = (v) => DateTime(v.year); - break; } _firstDate = normalizeDate(firstDate); _lastDate = normalizeDate(lastDate); @@ -118,15 +115,12 @@ class _HistogramState extends State with AutomaticKeepAliveClientMixi case DateLevel.ymd: xCount = xRange.inDays; incrementDate = (date) => DateTime(date.year, date.month, date.day + 1); - break; case DateLevel.ym: xCount = (xRange.inDays / 30.5).round(); incrementDate = (date) => DateTime(date.year, date.month + 1); - break; default: xCount = lastDate.year - firstDate.year; incrementDate = (date) => DateTime(date.year + 1); - break; } final yMax = entryCountPerDate.values.reduce(max).toDouble(); final xInterval = yMax / xCount; diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index 3f8812c78..d31760d64 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -82,7 +82,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.convert: return canWrite && !targetEntry.isVideo; case EntryAction.print: - return device.canPrint && !targetEntry.isVideo; + return !targetEntry.isVideo; case EntryAction.openMap: return !settings.useTvLayout && targetEntry.hasGps; case EntryAction.viewSource: @@ -181,61 +181,44 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix switch (action) { case EntryAction.info: ShowInfoPageNotification().dispatch(context); - break; case EntryAction.addShortcut: _addShortcut(context, targetEntry); - break; case EntryAction.copyToClipboard: appService.copyToClipboard(targetEntry.uri, targetEntry.bestTitle).then((success) { showFeedback(context, success ? context.l10n.genericSuccessFeedback : context.l10n.genericFailureFeedback); }); - break; case EntryAction.delete: _delete(context, targetEntry); - break; case EntryAction.restore: _move(context, targetEntry, moveType: MoveType.fromBin); - break; case EntryAction.convert: convert(context, {targetEntry}); - break; case EntryAction.print: EntryPrinter(targetEntry).print(context); - break; case EntryAction.rename: _rename(context, targetEntry); - break; case EntryAction.copy: _move(context, targetEntry, moveType: MoveType.copy); - break; case EntryAction.move: _move(context, targetEntry, moveType: MoveType.move); - break; case EntryAction.share: appService.shareEntries({targetEntry}).then((success) { if (!success) showNoMatchingAppDialog(context); }); - break; case EntryAction.toggleFavourite: targetEntry.toggleFavourite(); - break; // raster case EntryAction.rotateCCW: _rotate(context, targetEntry, clockwise: false); - break; case EntryAction.rotateCW: _rotate(context, targetEntry, clockwise: true); - break; case EntryAction.flip: _flip(context, targetEntry); - break; // vector case EntryAction.viewSource: _goToSourceViewer(context, targetEntry); - break; case EntryAction.lockViewer: const LockViewNotification(locked: true).dispatch(context); - break; // video case EntryAction.videoCaptureFrame: case EntryAction.videoToggleMute: @@ -254,31 +237,25 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix action: action, ).dispatch(context); } - break; case EntryAction.edit: appService.edit(targetEntry.uri, targetEntry.mimeType).then((success) { if (!success) showNoMatchingAppDialog(context); }); - break; case EntryAction.open: appService.open(targetEntry.uri, targetEntry.mimeTypeAnySubtype, forceChooser: true).then((success) { if (!success) showNoMatchingAppDialog(context); }); - break; case EntryAction.openMap: appService.openMap(targetEntry.latLng!).then((success) { if (!success) showNoMatchingAppDialog(context); }); - break; case EntryAction.setAs: appService.setAs(targetEntry.uri, targetEntry.mimeType).then((success) { if (!success) showNoMatchingAppDialog(context); }); - break; // platform case EntryAction.rotateScreen: _rotateScreen(context); - break; // metadata case EntryAction.editDate: case EntryAction.editLocation: @@ -291,11 +268,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.convertMotionPhotoToStillImage: case EntryAction.viewMotionPhotoVideo: _metadataActionDelegate.onActionSelected(context, targetEntry, collection, action); - break; // debug case EntryAction.debug: _goToDebug(context, targetEntry); - break; } } @@ -321,13 +296,11 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix final fields = await embeddedDataService.extractMotionPhotoImage(mainEntry); await _shareMotionPhotoPart(context, fields); } - break; case ShareAction.videoOnly: if (mainEntry.isMotionPhoto) { final fields = await embeddedDataService.extractMotionPhotoVideo(mainEntry); await _shareMotionPhotoPart(context, fields); } - break; } } @@ -379,14 +352,8 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } Future _rotateScreen(BuildContext context) async { - switch (context.read().orientation) { - case Orientation.landscape: - await windowService.requestOrientation(Orientation.portrait); - break; - case Orientation.portrait: - await windowService.requestOrientation(Orientation.landscape); - break; - } + final isPortrait = MediaQuery.orientationOf(context) == Orientation.portrait; + await windowService.requestOrientation(isPortrait ? Orientation.landscape : Orientation.portrait); } Future _delete(BuildContext context, AvesEntry targetEntry) async { diff --git a/lib/widgets/viewer/action/entry_info_action_delegate.dart b/lib/widgets/viewer/action/entry_info_action_delegate.dart index 33bb488e0..0487d219e 100644 --- a/lib/widgets/viewer/action/entry_info_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_info_action_delegate.dart @@ -99,40 +99,29 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi // general case EntryAction.editDate: await _editDate(context, targetEntry, collection); - break; case EntryAction.editLocation: await _editLocation(context, targetEntry, collection); - break; case EntryAction.editTitleDescription: await _editTitleDescription(context, targetEntry); - break; case EntryAction.editRating: await _editRating(context, targetEntry); - break; case EntryAction.editTags: await _editTags(context, targetEntry); - break; case EntryAction.removeMetadata: await _removeMetadata(context, targetEntry); - break; case EntryAction.exportMetadata: await _exportMetadata(context, targetEntry); - break; // GeoTIFF case EntryAction.showGeoTiffOnMap: await _showGeoTiffOnMap(context, targetEntry, collection); - break; // motion photo case EntryAction.convertMotionPhotoToStillImage: await _convertMotionPhotoToStillImage(context, targetEntry); - break; case EntryAction.viewMotionPhotoVideo: OpenEmbeddedDataNotification.motionPhotoVideo().dispatch(context); - break; // debug case EntryAction.debug: _goToDebug(context, targetEntry); - break; default: break; } @@ -262,6 +251,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi final mappedGeoTiff = MappedGeoTiff( info: info, entry: targetEntry, + devicePixelRatio: View.of(context).devicePixelRatio, ); if (!mappedGeoTiff.canOverlay) return; diff --git a/lib/widgets/viewer/action/video_action_delegate.dart b/lib/widgets/viewer/action/video_action_delegate.dart index bff1f5412..e9a9d454f 100644 --- a/lib/widgets/viewer/action/video_action_delegate.dart +++ b/lib/widgets/viewer/action/video_action_delegate.dart @@ -45,33 +45,24 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix switch (action) { case EntryAction.videoCaptureFrame: await _captureFrame(context, entry, controller); - break; case EntryAction.videoToggleMute: await controller.mute(!controller.isMuted); - break; case EntryAction.videoSelectStreams: await _showStreamSelectionDialog(context, controller); - break; case EntryAction.videoSetSpeed: await _showSpeedDialog(context, controller); - break; case EntryAction.videoSettings: await _showSettings(context, controller); - break; case EntryAction.videoTogglePlay: await _togglePlayPause(context, controller); - break; case EntryAction.videoReplay10: await controller.seekTo(controller.currentPosition - 10000); - break; case EntryAction.videoSkip10: await controller.seekTo(controller.currentPosition + 10000); - break; case EntryAction.openVideo: await appService.open(entry.uri, entry.mimeTypeAnySubtype, forceChooser: false).then((success) { if (!success) showNoMatchingAppDialog(context); }); - break; default: throw UnsupportedError('$action is not a video action'); } diff --git a/lib/widgets/viewer/entry_horizontal_pager.dart b/lib/widgets/viewer/entry_horizontal_pager.dart index c491edad4..c6a11a9f2 100644 --- a/lib/widgets/viewer/entry_horizontal_pager.dart +++ b/lib/widgets/viewer/entry_horizontal_pager.dart @@ -12,7 +12,6 @@ import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -57,7 +56,7 @@ class _MultiEntryScrollerState extends State with AutomaticK scrollDirection: Axis.horizontal, controller: pageController, physics: MagnifierScrollerPhysics( - gestureSettings: context.select((mq) => mq.gestureSettings), + gestureSettings: MediaQuery.gestureSettingsOf(context), parent: const BouncingScrollPhysics(), ), onPageChanged: widget.onPageChanged, diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index f03de1f69..7e6cd8129 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -22,7 +22,6 @@ import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; import 'package:aves_model/aves_model.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; @@ -41,7 +40,7 @@ class ViewerVerticalPageView extends StatefulWidget { // critically damped spring a bit stiffer than `ScrollPhysics._kDefaultSpring` static final spring = SpringDescription.withDampingRatio( mass: 0.5, - stiffness: 200.0, + stiffness: 140.0, ratio: 1.0, ); @@ -187,7 +186,7 @@ class _ViewerVerticalPageViewState extends State { scrollDirection: Axis.vertical, controller: widget.verticalPager, physics: MagnifierScrollerPhysics( - gestureSettings: context.select((mq) => mq.gestureSettings), + gestureSettings: MediaQuery.gestureSettingsOf(context), parent: SpringyScrollPhysics( spring: ViewerVerticalPageView.spring, ), @@ -384,13 +383,10 @@ class _ViewerVerticalPageViewState extends State { switch (intent.type) { case TvPlayPauseType.play: toggle = !controller.isPlaying; - break; case TvPlayPauseType.pause: toggle = controller.isPlaying; - break; case TvPlayPauseType.toggle: toggle = true; - break; } if (toggle) { VideoActionNotification( diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index f5c601b0d..03093cb45 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -286,14 +286,11 @@ class _EntryViewerStackState extends State with EntryViewContr switch (AvesApp.lifecycleStateNotifier.value) { case AppLifecycleState.inactive: _onAppInactive(); - break; case AppLifecycleState.paused: case AppLifecycleState.detached: pauseVideoControllers(); - break; case AppLifecycleState.resumed: availability.onResume(); - break; } } @@ -525,19 +522,16 @@ class _EntryViewerStackState extends State with EntryViewContr switch (notification.moveType) { case MoveType.move: _onEntryRemoved(context, entries); - break; case MoveType.toBin: if (!isBin) { _onEntryRemoved(context, entries); } - break; case MoveType.fromBin: if (isBin) { _onEntryRemoved(context, entries); } else { _onEntryRestored(entries); } - break; case MoveType.copy: case MoveType.export: break; @@ -639,15 +633,12 @@ class _EntryViewerStackState extends State with EntryViewContr case transitionPage: dismissFeedback(context); _popVisual(); - break; case imagePage: reportService.log('Nav move to Image page'); - break; case infoPage: reportService.log('Nav move to Info page'); // prevent hero when viewer is offscreen _heroInfoNotifier.value = null; - break; } } @@ -821,10 +812,8 @@ class _EntryViewerStackState extends State with EntryViewContr case MaxBrightness.never: case MaxBrightness.viewerOnly: await ScreenBrightness().resetScreenBrightness(); - break; case MaxBrightness.always: await ScreenBrightness().setScreenBrightness(1); - break; } if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { await windowService.keepScreenOn(false); @@ -843,8 +832,7 @@ class _EntryViewerStackState extends State with EntryViewContr final entrySize = videoController.entry.displaySize; final aspectRatio = Rational(entrySize.width.round(), entrySize.height.round()); - final mq = context.read(); - final viewSize = mq.size * mq.devicePixelRatio; + final viewSize = MediaQuery.sizeOf(context) * MediaQuery.devicePixelRatioOf(context); final fittedSize = applyBoxFit(BoxFit.contain, entrySize, viewSize).destination; final sourceRectHint = Rectangle( ((viewSize.width - fittedSize.width) / 2).round(), diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index db32c6994..73445653d 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -7,6 +7,7 @@ import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/filters/date.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/rating.dart'; @@ -115,6 +116,7 @@ class _BasicSectionState extends State { Widget _buildChips(BuildContext context) { final entry = widget.entry; final tags = entry.tags.toList()..sort(compareAsciiUpperCaseNatural); + final date = entry.bestDate; final album = entry.directory; final filters = { MimeFilter(entry.mimeType), @@ -125,6 +127,7 @@ class _BasicSectionState extends State { if (entry.isImage && entry.is360) TypeFilter.panorama, if (entry.isVideo && entry.is360) TypeFilter.sphericalVideo, if (entry.isVideo && !entry.is360) MimeFilter.video, + if (date != null) DateFilter(DateLevel.ymd, date), if (album != null) AlbumFilter(album, collection?.source.getAlbumDisplayName(context, album)), if (entry.rating != 0) RatingFilter(entry.rating), ...tags.map(TagFilter.new), @@ -202,21 +205,18 @@ class _BasicSectionState extends State { onChooserValue: (rating) => actionDelegate.quickRate(context, entry, rating), onPressed: onPressed, ); - break; case EntryAction.editTags: button = TagButton( blurred: false, onChooserValue: (filter) => actionDelegate.quickTag(context, entry, filter), onPressed: onPressed, ); - break; default: button = IconButton( icon: action.getIcon(), onPressed: onPressed, tooltip: action.getText(context), ); - break; } return Stack( children: [ @@ -306,7 +306,7 @@ class _BasicInfoState extends State<_BasicInfo> { final l10n = context.l10n; final infoUnknown = l10n.viewerInfoUnknown; final locale = l10n.localeName; - final use24hour = context.select((v) => v.alwaysUse24HourFormat); + final use24hour = MediaQuery.alwaysUse24HourFormatOf(context); // TODO TLAD line break on all characters for the following fields when this is fixed: https://github.com/flutter/flutter/issues/61081 // inserting ZWSP (\u200B) between characters does help, but it messes with width and height computation (another Flutter issue) diff --git a/lib/widgets/viewer/info/embedded/embedded_data_opener.dart b/lib/widgets/viewer/info/embedded/embedded_data_opener.dart index 287a712fb..54faa81f6 100644 --- a/lib/widgets/viewer/info/embedded/embedded_data_opener.dart +++ b/lib/widgets/viewer/info/embedded/embedded_data_opener.dart @@ -42,16 +42,12 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin { switch (notification.source) { case EmbeddedDataSource.googleDevice: fields = await embeddedDataService.extractGoogleDeviceItem(entry, notification.dataUri!); - break; case EmbeddedDataSource.motionPhotoVideo: fields = await embeddedDataService.extractMotionPhotoVideo(entry); - break; case EmbeddedDataSource.videoCover: fields = await embeddedDataService.extractVideoEmbeddedPicture(entry); - break; case EmbeddedDataSource.xmp: fields = await embeddedDataService.extractXmpDataProp(entry, notification.props, notification.mimeType); - break; } if (!fields.containsKey('mimeType') || !fields.containsKey('uri')) { showFeedback(context, context.l10n.viewerInfoOpenEmbeddedFailureFeedback); diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 440427f9f..1529e11bb 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -61,39 +61,34 @@ class _InfoPageState extends State { bottom: false, child: NotificationListener( onNotification: _handleTopScroll, - child: Selector( - selector: (context, mq) => mq.size.width, - builder: (context, mqWidth, child) { - return ValueListenableBuilder( - valueListenable: widget.entryNotifier, - builder: (context, mainEntry, child) { - if (mainEntry == null) return const SizedBox(); + child: ValueListenableBuilder( + valueListenable: widget.entryNotifier, + builder: (context, mainEntry, child) { + if (mainEntry == null) return const SizedBox(); - final isSelecting = context.select?, bool>((v) => v?.isSelecting ?? false); - Widget _buildContent({AvesEntry? pageEntry}) { - final targetEntry = pageEntry ?? mainEntry; - return EmbeddedDataOpener( - enabled: !isSelecting, - entry: targetEntry, - child: _InfoPageContent( - collection: widget.collection, - entry: targetEntry, - isScrollingNotifier: widget.isScrollingNotifier, - scrollController: _scrollController, - split: mqWidth > splitScreenWidthThreshold, - goToViewer: _goToViewer, - ), - ); - } + final isSelecting = context.select?, bool>((v) => v?.isSelecting ?? false); + Widget _buildContent({AvesEntry? pageEntry}) { + final targetEntry = pageEntry ?? mainEntry; + return EmbeddedDataOpener( + enabled: !isSelecting, + entry: targetEntry, + child: _InfoPageContent( + collection: widget.collection, + entry: targetEntry, + isScrollingNotifier: widget.isScrollingNotifier, + scrollController: _scrollController, + split: MediaQuery.sizeOf(context).width > splitScreenWidthThreshold, + goToViewer: _goToViewer, + ), + ); + } - return mainEntry.isBurst - ? PageEntryBuilder( - multiPageController: context.read().getController(mainEntry), - builder: (pageEntry) => _buildContent(pageEntry: pageEntry), - ) - : _buildContent(); - }, - ); + return mainEntry.isBurst + ? PageEntryBuilder( + multiPageController: context.read().getController(mainEntry), + builder: (pageEntry) => _buildContent(pageEntry: pageEntry), + ) + : _buildContent(); }, ), ), diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index 6e5abc3f4..1010fc999 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -89,6 +89,7 @@ class _LocationSectionState extends State { child: GeoMap( controller: _mapController, entries: [entry], + availableSize: MediaQuery.sizeOf(context), isAnimatingNotifier: widget.isScrollingNotifier, onUserZoomChange: (zoom) => settings.infoMapZoom = zoom.roundToDouble(), onMarkerTap: collection != null && canNavigate ? (location, entry) => _openMapPage(context) : null, diff --git a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart index b999e44ce..0fa09df08 100644 --- a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart +++ b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart @@ -98,16 +98,13 @@ class MetadataDirTileBody extends StatelessWidget { switch (dirName) { case SvgMetadataService.metadataDirectory: linkHandlers = getSvgLinkHandlers(tags); - break; case MetadataDirectory.coverDirectory: linkHandlers = getVideoCoverLinkHandlers(tags); - break; case MetadataDirectory.geoTiffDirectory: tags = SplayTreeMap.from(tags.map((name, value) { final tag = GeoTiffDirectory.tagForName(name); return MapEntry(name, GeoTiffDirectory.formatValue(tag, value)); })); - break; } children = [ diff --git a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart index 77d0407fc..f4d3f2ba5 100644 --- a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart +++ b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart @@ -4,7 +4,6 @@ import 'dart:typed_data'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/services/common/services.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class MetadataThumbnails extends StatefulWidget { final AvesEntry entry; @@ -44,7 +43,7 @@ class _MetadataThumbnailsState extends State { children: snapshot.data!.map((bytes) { return Image.memory( bytes, - scale: context.select((mq) => mq.devicePixelRatio), + scale: MediaQuery.devicePixelRatioOf(context), ); }).toList(), ), diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index d80348978..cb9339f2a 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -20,7 +20,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; -import 'package:tuple/tuple.dart'; class ViewerBottomOverlay extends StatefulWidget { final List entries; @@ -297,19 +296,14 @@ class ExtraBottomOverlay extends StatelessWidget { @override Widget build(BuildContext context) { - final mq = context.select>((mq) => Tuple3(mq.size.width, mq.viewInsets, mq.viewPadding)); - final mqWidth = mq.item1; - final mqViewInsets = mq.item2; - final mqViewPadding = mq.item3; - - final viewInsets = this.viewInsets ?? mqViewInsets; - final viewPadding = this.viewPadding ?? mqViewPadding; + final viewInsets = this.viewInsets ?? MediaQuery.viewInsetsOf(context); + final viewPadding = this.viewPadding ?? MediaQuery.viewPaddingOf(context); final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + const EdgeInsets.symmetric(horizontal: 8.0); return Padding( padding: safePadding, child: SizedBox( - width: mqWidth - safePadding.horizontal, + width: MediaQuery.sizeOf(context).width - safePadding.horizontal, child: child, ), ); diff --git a/lib/widgets/viewer/overlay/details/date.dart b/lib/widgets/viewer/overlay/details/date.dart index 62abba441..e6e77fb55 100644 --- a/lib/widgets/viewer/overlay/details/date.dart +++ b/lib/widgets/viewer/overlay/details/date.dart @@ -9,7 +9,6 @@ import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/overlay/details/details.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class OverlayDateRow extends StatelessWidget { final AvesEntry entry; @@ -24,7 +23,7 @@ class OverlayDateRow extends StatelessWidget { @override Widget build(BuildContext context) { final locale = context.l10n.localeName; - final use24hour = context.select((v) => v.alwaysUse24HourFormat); + final use24hour = MediaQuery.alwaysUse24HourFormatOf(context); final date = entry.bestDate; final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable; diff --git a/lib/widgets/viewer/overlay/details/details.dart b/lib/widgets/viewer/overlay/details/details.dart index c7b4bba56..b84fc33f4 100644 --- a/lib/widgets/viewer/overlay/details/details.dart +++ b/lib/widgets/viewer/overlay/details/details.dart @@ -150,81 +150,79 @@ class ViewerDetailOverlayContent extends StatelessWidget { @override Widget build(BuildContext context) { + return AnimatedBuilder( + animation: pageEntry.metadataChangeNotifier, + builder: (context, child) => DefaultTextStyle( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + shadows: shadows(context), + ), + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + child: Padding( + padding: padding, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildRows(context), + ), + ), + ), + ); + } + + List _buildRows(BuildContext context) { final infoMaxWidth = availableWidth - padding.horizontal; final showRatingTags = settings.showOverlayRatingTags; final showShootingDetails = settings.showOverlayShootingDetails; final showDescription = settings.showOverlayDescription; - return AnimatedBuilder( - animation: pageEntry.metadataChangeNotifier, - builder: (context, child) { - final positionTitle = OverlayPositionTitleRow( - entry: pageEntry, - collectionPosition: position, - multiPageController: multiPageController, - ); - return DefaultTextStyle( - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - shadows: shadows(context), - ), - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - child: Padding( - padding: padding, - child: Selector( - selector: (context, mq) => mq.orientation, - builder: (context, orientation, child) { - final twoColumns = orientation == Orientation.landscape && infoMaxWidth / 2 > _subRowMinWidth; - final subRowWidth = twoColumns ? min(_subRowMinWidth, infoMaxWidth / 2) : infoMaxWidth; - final collapsedShooting = twoColumns && showShootingDetails; - final collapsedLocation = twoColumns && !showShootingDetails; + final isLandscape = MediaQuery.orientationOf(context) == Orientation.landscape; + final twoColumns = isLandscape && infoMaxWidth / 2 > _subRowMinWidth; + final subRowWidth = twoColumns ? min(_subRowMinWidth, infoMaxWidth / 2) : infoMaxWidth; + final collapsedShooting = twoColumns && showShootingDetails; + final collapsedLocation = twoColumns && !showShootingDetails; - final rows = []; - if (positionTitle.isNotEmpty) { - rows.add(OverlayRowExpander( - expandedNotifier: expandedNotifier, - child: positionTitle, - )); - rows.add(const SizedBox(height: _interRowPadding)); - } - if (twoColumns) { - rows.add( - Row( - children: [ - _buildDateSubRow(subRowWidth), - if (collapsedShooting) _buildShootingSubRow(context, subRowWidth), - if (collapsedLocation) _buildLocationSubRow(context, subRowWidth), - ], - ), - ); - } else { - rows.add(_buildDateSubRow(subRowWidth)); - if (showShootingDetails) { - rows.add(_buildShootingFullRow(context, subRowWidth)); - } - } - if (!collapsedLocation) { - rows.add(_buildLocationFullRow(context)); - } - if (showRatingTags) { - rows.add(_buildRatingTagsFullRow(context)); - } - if (showDescription) { - rows.add(_buildDescriptionFullRow(context)); - } - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: rows, - ); - }, - ), - ), - ); - }, + final positionTitle = OverlayPositionTitleRow( + entry: pageEntry, + collectionPosition: position, + multiPageController: multiPageController, ); + + final rows = []; + if (positionTitle.isNotEmpty) { + rows.add(OverlayRowExpander( + expandedNotifier: expandedNotifier, + child: positionTitle, + )); + rows.add(const SizedBox(height: _interRowPadding)); + } + if (twoColumns) { + rows.add( + Row( + children: [ + _buildDateSubRow(subRowWidth), + if (collapsedShooting) _buildShootingSubRow(context, subRowWidth), + if (collapsedLocation) _buildLocationSubRow(context, subRowWidth), + ], + ), + ); + } else { + rows.add(_buildDateSubRow(subRowWidth)); + if (showShootingDetails) { + rows.add(_buildShootingFullRow(context, subRowWidth)); + } + } + if (!collapsedLocation) { + rows.add(_buildLocationFullRow(context)); + } + if (showRatingTags) { + rows.add(_buildRatingTagsFullRow(context)); + } + if (showDescription) { + rows.add(_buildDescriptionFullRow(context)); + } + return rows; } Widget _buildDateSubRow(double subRowWidth) => SizedBox( diff --git a/lib/widgets/viewer/overlay/details/rating_tags.dart b/lib/widgets/viewer/overlay/details/rating_tags.dart index 8cda1194d..f2a93d3f5 100644 --- a/lib/widgets/viewer/overlay/details/rating_tags.dart +++ b/lib/widgets/viewer/overlay/details/rating_tags.dart @@ -23,13 +23,10 @@ class OverlayRatingTagsRow extends AnimatedWidget { switch (rating) { case -1: ratingString = context.l10n.filterRatingRejectedLabel; - break; case 0: ratingString = ''; - break; default: ratingString = '${'★' * rating}${'☆' * (5 - rating)}'; - break; } final textScaleFactor = MediaQuery.textScaleFactorOf(context); diff --git a/lib/widgets/viewer/overlay/minimap.dart b/lib/widgets/viewer/overlay/minimap.dart index b3f5de899..59ca726f8 100644 --- a/lib/widgets/viewer/overlay/minimap.dart +++ b/lib/widgets/viewer/overlay/minimap.dart @@ -1,8 +1,12 @@ import 'dart:math'; -import 'package:aves/widgets/viewer/visual/state.dart'; +import 'package:aves/model/view_state.dart'; +import 'package:aves/widgets/editor/transform/controller.dart'; +import 'package:aves/widgets/editor/transform/transformation.dart'; +import 'package:aves_utils/aves_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class Minimap extends StatelessWidget { final ValueNotifier viewStateNotifier; @@ -23,16 +27,22 @@ class Minimap extends StatelessWidget { final viewportSize = viewState.viewportSize; final contentSize = viewState.contentSize; if (viewportSize == null || contentSize == null) return const SizedBox(); - return CustomPaint( - painter: MinimapPainter( - viewportSize: viewportSize, - contentSize: contentSize, - viewCenterOffset: viewState.position, - viewScale: viewState.scale!, - minimapBorderColor: Colors.white30, - ), - size: minimapSize, - ); + return StreamBuilder( + stream: context.select>((v) => v?.transformationStream ?? Stream.value(null)), + builder: (context, snapshot) { + final transformation = snapshot.data; + return CustomPaint( + painter: MinimapPainter( + viewportSize: viewportSize, + contentSize: contentSize, + viewCenterOffset: viewState.position, + viewScale: viewState.scale!, + transformation: transformation, + minimapBorderColor: Colors.white30, + ), + size: minimapSize, + ); + }); }, ), ); @@ -43,16 +53,30 @@ class MinimapPainter extends CustomPainter { final Size contentSize, viewportSize; final Offset viewCenterOffset; final double viewScale; + final Transformation? transformation; final Color minimapBorderColor, viewportBorderColor; - const MinimapPainter({ + late final Paint fill, minimapStroke, viewportStroke; + + MinimapPainter({ required this.viewportSize, required this.contentSize, required this.viewCenterOffset, required this.viewScale, + this.transformation, this.minimapBorderColor = Colors.white, this.viewportBorderColor = Colors.white, - }); + }) { + fill = Paint() + ..style = PaintingStyle.fill + ..color = const Color(0x33000000); + minimapStroke = Paint() + ..style = PaintingStyle.stroke + ..color = minimapBorderColor; + viewportStroke = Paint() + ..style = PaintingStyle.stroke + ..color = viewportBorderColor; + } @override void paint(Canvas canvas, Size size) { @@ -64,37 +88,56 @@ class MinimapPainter extends CustomPainter { // hide minimap when image is in full view if (viewportSize + const Offset(precisionErrorTolerance, precisionErrorTolerance) >= viewSize) return; + final canvasCenter = size.center(Offset.zero); final canvasScale = size.longestSide / viewSize.longestSide; final scaledContentSize = viewSize * canvasScale; final scaledViewportSize = viewportSize * canvasScale; final contentRect = Rect.fromCenter( - center: size.center(Offset.zero), + center: canvasCenter, width: scaledContentSize.width, height: scaledContentSize.height, ); final viewportRect = Rect.fromCenter( - center: size.center(Offset.zero) - viewCenterOffset * canvasScale, + center: canvasCenter - viewCenterOffset * canvasScale, width: min(scaledContentSize.width, scaledViewportSize.width), height: min(scaledContentSize.height, scaledViewportSize.height), ); - canvas.translate((contentRect.width - size.width) / 2, (contentRect.height - size.height) / 2); + Matrix4? transformMatrix; + if (transformation != null) { + final viewportCenter = viewportRect.center; + final transformOrigin = viewportCenter; + transformMatrix = Matrix4.identity() + ..translate(transformOrigin.dx, transformOrigin.dy) + ..multiply(transformation!.matrix) + ..translate(-transformOrigin.dx, -transformOrigin.dy); + final transViewportCenter = transformMatrix.transformOffset(viewportCenter); + final transContentCenter = transformMatrix.transformOffset(contentRect.center); - final fill = Paint() - ..style = PaintingStyle.fill - ..color = const Color(0x33000000); - final minimapStroke = Paint() - ..style = PaintingStyle.stroke - ..color = minimapBorderColor; - final viewportStroke = Paint() - ..style = PaintingStyle.stroke - ..color = viewportBorderColor; + final minimapTranslation = size / 2 + (transViewportCenter - transContentCenter - viewportCenter); + canvas.translate(minimapTranslation.width, minimapTranslation.height); + } else { + canvas.translate((contentRect.width - size.width) / 2, (contentRect.height - size.height) / 2); + } canvas.drawRect(viewportRect, fill); + + if (transformMatrix != null) { + canvas.transform(transformMatrix.storage); + _drawContentRect(canvas, contentRect); + transformMatrix.invert(); + canvas.transform(transformMatrix.storage); + } else { + _drawContentRect(canvas, contentRect); + } + + canvas.drawRect(viewportRect, viewportStroke); + } + + void _drawContentRect(Canvas canvas, Rect contentRect) { canvas.drawRect(contentRect, fill); canvas.drawRect(contentRect, minimapStroke); - canvas.drawRect(viewportRect, viewportStroke); } @override diff --git a/lib/widgets/viewer/overlay/video/progress_bar.dart b/lib/widgets/viewer/overlay/video/progress_bar.dart index 62a21c974..cdf245690 100644 --- a/lib/widgets/viewer/overlay/video/progress_bar.dart +++ b/lib/widgets/viewer/overlay/video/progress_bar.dart @@ -9,7 +9,6 @@ import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves_video/aves_video.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class VideoProgressBar extends StatefulWidget { final AvesVideoController? controller; @@ -155,16 +154,14 @@ class _VideoProgressBarState extends State { Widget _buildMuteIndicator() => StreamBuilder( stream: controller?.volumeStream ?? Stream.value(1.0), builder: (context, snapshot) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); final isMuted = controller?.isMuted ?? false; return isMuted ? Padding( padding: const EdgeInsetsDirectional.only(end: 8), - child: Selector( - selector: (context, mq) => mq.textScaleFactor, - builder: (context, textScaleFactor, child) => Icon( - AIcons.mute, - size: 16 * textScaleFactor, - ), + child: Icon( + AIcons.mute, + size: 16 * textScaleFactor, ), ) : const SizedBox(); diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart index fbb31c322..b64a91f6c 100644 --- a/lib/widgets/viewer/overlay/viewer_buttons.dart +++ b/lib/widgets/viewer/overlay/viewer_buttons.dart @@ -327,19 +327,14 @@ class ViewerButtonRowContent extends StatelessWidget { switch (action) { case EntryAction.videoCaptureFrame: enabled = videoController?.canCaptureFrameNotifier.value ?? false; - break; case EntryAction.videoToggleMute: enabled = videoController?.canMuteNotifier.value ?? false; - break; case EntryAction.videoSelectStreams: enabled = videoController?.canSelectStreamNotifier.value ?? false; - break; case EntryAction.videoSetSpeed: enabled = videoController?.canSetSpeedNotifier.value ?? false; - break; default: enabled = true; - break; } Widget? child; @@ -349,22 +344,18 @@ class ViewerButtonRowContent extends StatelessWidget { entries: {favouriteTargetEntry}, isMenuItem: true, ); - break; case EntryAction.videoToggleMute: child = MuteToggler( controller: videoController, isMenuItem: true, ); - break; case EntryAction.videoTogglePlay: child = PlayToggler( controller: videoController, isMenuItem: true, ); - break; default: child = MenuRow(text: action.getText(context), icon: action.getIcon()); - break; } return PopupMenuItem( value: action, @@ -448,7 +439,6 @@ class ViewerButtonRowContent extends StatelessWidget { onChooserValue: (album) => actionDelegate.quickMove(context, album, copy: true), onPressed: onPressed, ); - break; case EntryAction.move: child = MoveButton( copy: false, @@ -456,7 +446,6 @@ class ViewerButtonRowContent extends StatelessWidget { onChooserValue: (album) => actionDelegate.quickMove(context, album, copy: false), onPressed: onPressed, ); - break; case EntryAction.share: child = ShareButton( blurred: blurred, @@ -465,7 +454,6 @@ class ViewerButtonRowContent extends StatelessWidget { focusNode: focusNode, onPressed: onPressed, ); - break; case EntryAction.toggleFavourite: final favouriteTargetEntry = mainEntry.isBurst ? pageEntry : mainEntry; child = FavouriteToggler( @@ -473,30 +461,24 @@ class ViewerButtonRowContent extends StatelessWidget { focusNode: focusNode, onPressed: onPressed, ); - break; case EntryAction.videoToggleMute: child = MuteToggler( controller: videoController, focusNode: focusNode, onPressed: onPressed, ); - break; case EntryAction.videoTogglePlay: child = PlayToggler( controller: videoController, focusNode: focusNode, onPressed: onPressed, ); - break; case EntryAction.videoCaptureFrame: child = _buildFromListenable(videoController?.canCaptureFrameNotifier); - break; case EntryAction.videoSelectStreams: child = _buildFromListenable(videoController?.canSelectStreamNotifier); - break; case EntryAction.videoSetSpeed: child = _buildFromListenable(videoController?.canSetSpeedNotifier); - break; case EntryAction.editRating: child = RateButton( blurred: blurred, @@ -504,7 +486,6 @@ class ViewerButtonRowContent extends StatelessWidget { focusNode: focusNode, onPressed: onPressed, ); - break; case EntryAction.editTags: child = TagButton( blurred: blurred, @@ -512,7 +493,6 @@ class ViewerButtonRowContent extends StatelessWidget { focusNode: focusNode, onPressed: onPressed, ); - break; default: child = IconButton( icon: action.getIcon(), @@ -520,7 +500,6 @@ class ViewerButtonRowContent extends StatelessWidget { focusNode: focusNode, tooltip: action.getText(context), ); - break; } return child; } diff --git a/lib/widgets/viewer/overlay/wallpaper_buttons.dart b/lib/widgets/viewer/overlay/wallpaper_buttons.dart index 2ff880700..f7e70483a 100644 --- a/lib/widgets/viewer/overlay/wallpaper_buttons.dart +++ b/lib/widgets/viewer/overlay/wallpaper_buttons.dart @@ -103,7 +103,7 @@ class WallpaperButtons extends StatelessWidget with FeedbackMixin { final center = (contentSize / 2 - viewState.position / scale) as Size; final regionSize = viewportSize / scale; final regionTopLeft = (center - regionSize / 2) as Offset; - return Rect.fromLTWH(regionTopLeft.dx, regionTopLeft.dy, regionSize.width, regionSize.height); + return regionTopLeft & regionSize; } Future _getBytes(BuildContext context, Rect displayRegion) async { @@ -209,14 +209,11 @@ class WallpaperButtons extends StatelessWidget with FeedbackMixin { case 90: canvas.translate(-imageDisplaySize.width, imageDisplaySize.height); canvas.rotate(degToRadian(270)); - break; case 180: canvas.translate(0, imageDisplaySize.height); canvas.rotate(degToRadian(180)); - break; case 270: canvas.rotate(degToRadian(90)); - break; } } else { switch (rotationDegrees) { @@ -224,13 +221,10 @@ class WallpaperButtons extends StatelessWidget with FeedbackMixin { canvas.translate(imageDisplaySize.width, 0); cropDx = -displayRegion.top.toDouble(); cropDy = displayRegion.left.toDouble(); - break; case 180: canvas.translate(imageDisplaySize.width, imageDisplaySize.height); - break; case 270: canvas.translate(0, imageDisplaySize.height); - break; } if (rotationDegrees != 0) { canvas.rotate(degToRadian(rotationDegrees.toDouble())); diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index 71067e16d..464dc5dc8 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -156,11 +156,9 @@ class _PanoramaPageState extends State { switch (_sensorControl.value) { case SensorControl.None: _sensorControl.value = SensorControl.AbsoluteOrientation; - break; case SensorControl.AbsoluteOrientation: case SensorControl.Orientation: _sensorControl.value = SensorControl.None; - break; } } diff --git a/lib/widgets/viewer/slideshow_page.dart b/lib/widgets/viewer/slideshow_page.dart index d4139d669..d05186d6f 100644 --- a/lib/widgets/viewer/slideshow_page.dart +++ b/lib/widgets/viewer/slideshow_page.dart @@ -124,13 +124,10 @@ class _SlideshowPageState extends State { switch (action) { case SlideshowAction.resume: _viewerController.autopilot = true; - break; case SlideshowAction.showInCollection: _showInCollection(); - break; case SlideshowAction.settings: _showSettings(context); - break; } } diff --git a/lib/widgets/viewer/video/fijkplayer.dart b/lib/widgets/viewer/video/fijkplayer.dart index bd373c087..80d65934e 100644 --- a/lib/widgets/viewer/video/fijkplayer.dart +++ b/lib/widgets/viewer/video/fijkplayer.dart @@ -270,13 +270,10 @@ class IjkPlayerAvesVideoController extends AvesVideoController { if (width != null && height != null) { videoStreamCount++; } - break; case MediaStreamType.audio: audioStreamCount++; - break; case MediaStreamType.text: textStreamCount++; - break; } } }); @@ -303,9 +300,6 @@ class IjkPlayerAvesVideoController extends AvesVideoController { if (fields == null) return; final event = fields['event'] as String?; switch (event) { - case 'volume': - // ignore - break; case 'audiofocus': final value = fields['value'] as int?; if (value != null) { @@ -313,9 +307,10 @@ class IjkPlayerAvesVideoController extends AvesVideoController { case _audioFocusLoss: case _audioFocusRequestFailed: pause(); - break; } } + case 'volume': + // ignore break; } } @@ -538,11 +533,9 @@ extension ExtraFijkPlayer on FijkPlayer { case FijkState.prepared: removeListener(onChanged); completer.complete(); - break; case FijkState.error: removeListener(onChanged); completer.completeError(value.exception); - break; default: break; } diff --git a/lib/widgets/viewer/video/flutter_vlc_player.dart b/lib/widgets/viewer/video/flutter_vlc_player.dart index f9de65fbe..6fb16fe06 100644 --- a/lib/widgets/viewer/video/flutter_vlc_player.dart +++ b/lib/widgets/viewer/video/flutter_vlc_player.dart @@ -83,7 +83,7 @@ // // do not use `Magnifier` with `applyScale` enabled when using this widget, // // as the original video size will be used to create the `PlatformView` // // and a virtual display larger than the device screen may crash the app -// final mqWidth = context.select((mq) => mq.size.width); +// final mqWidth = MediaQuery.sizeOf(context).width; // final displaySize = entry.displaySize; // final ratio = mqWidth / displaySize.width; // return SizedBox.fromSize( diff --git a/lib/widgets/viewer/visual/conductor.dart b/lib/widgets/viewer/visual/conductor.dart index 9f07652e2..26f6a3b96 100644 --- a/lib/widgets/viewer/visual/conductor.dart +++ b/lib/widgets/viewer/visual/conductor.dart @@ -1,5 +1,5 @@ import 'package:aves/model/entry/entry.dart'; -import 'package:aves/widgets/viewer/visual/state.dart'; +import 'package:aves/model/view_state.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -35,7 +35,7 @@ class ViewStateConductor { maxScale: initialScale, initialScale: initialScale, viewportSize: _viewportSize, - childSize: entry.displaySize, + contentSize: entry.displaySize, ).initialScale, viewportSize: _viewportSize, contentSize: entry.displaySize, diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 30bf70574..12b216852 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -18,7 +18,7 @@ import 'package:aves/widgets/viewer/video/conductor.dart'; import 'package:aves/widgets/viewer/visual/conductor.dart'; import 'package:aves/widgets/viewer/visual/error.dart'; import 'package:aves/widgets/viewer/visual/raster.dart'; -import 'package:aves/widgets/viewer/visual/state.dart'; +import 'package:aves/model/view_state.dart'; import 'package:aves/widgets/viewer/visual/vector.dart'; import 'package:aves/widgets/viewer/visual/video/cover.dart'; import 'package:aves/widgets/viewer/visual/video/subtitle/subtitle.dart'; @@ -37,6 +37,8 @@ class EntryPageView extends StatefulWidget { final VoidCallback? onDisposed; static const decorationCheckSize = 20.0; + static const rasterMaxScale = ScaleLevel(factor: 5); + static const vectorMaxScale = ScaleLevel(factor: 25); const EntryPageView({ super.key, @@ -63,9 +65,6 @@ class _EntryPageViewState extends State with SingleTickerProvider ViewerController get viewerController => widget.viewerController; - static const rasterMaxScale = ScaleLevel(factor: 5); - static const vectorMaxScale = ScaleLevel(factor: 25); - @override void initState() { super.initState(); @@ -180,7 +179,7 @@ class _EntryPageViewState extends State with SingleTickerProvider Widget _buildSvgView() { return _buildMagnifier( - maxScale: vectorMaxScale, + maxScale: EntryPageView.vectorMaxScale, scaleStateCycle: _vectorScaleStateCycle, applyScale: false, child: VectorImageView( @@ -382,7 +381,7 @@ class _EntryPageViewState extends State with SingleTickerProvider Widget _buildMagnifier({ AvesMagnifierController? controller, Size? displaySize, - ScaleLevel maxScale = rasterMaxScale, + ScaleLevel maxScale = EntryPageView.rasterMaxScale, ScaleStateCycle scaleStateCycle = defaultScaleStateCycle, bool applyScale = true, MagnifierGestureScaleStartCallback? onScaleStart, @@ -398,7 +397,7 @@ class _EntryPageViewState extends State with SingleTickerProvider // key includes modified date to refresh when the image is modified by metadata (e.g. rotated) key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'), controller: controller ?? _magnifierController, - childSize: displaySize ?? entry.displaySize, + contentSize: displaySize ?? entry.displaySize, allowOriginalScaleBeyondRange: !isWallpaperMode, minScale: minScale, maxScale: maxScale, @@ -419,16 +418,12 @@ class _EntryPageViewState extends State with SingleTickerProvider switch (direction) { case AxisDirection.left: const ShowPreviousEntryNotification(animate: true).dispatch(context); - break; case AxisDirection.right: const ShowNextEntryNotification(animate: true).dispatch(context); - break; case AxisDirection.up: PopVisualNotification().dispatch(context); - break; case AxisDirection.down: ShowInfoPageNotification().dispatch(context); - break; } } @@ -456,24 +451,18 @@ class _EntryPageViewState extends State with SingleTickerProvider switch (event.command) { case MediaCommand.play: videoController.play(); - break; case MediaCommand.pause: videoController.pause(); - break; case MediaCommand.skipToNext: ShowNextVideoNotification().dispatch(context); - break; case MediaCommand.skipToPrevious: ShowPreviousVideoNotification().dispatch(context); - break; case MediaCommand.stop: videoController.pause(); - break; case MediaCommand.seek: if (event is MediaSeekCommandEvent) { videoController.seekTo(event.position); } - break; } } @@ -487,19 +476,13 @@ class _EntryPageViewState extends State with SingleTickerProvider void _onViewScaleBoundariesChanged(ScaleBoundaries v) { _viewStateNotifier.value = _viewStateNotifier.value.copyWith( viewportSize: v.viewportSize, - contentSize: v.childSize, + contentSize: v.contentSize, ); } double? _getSideRatio() { - switch (context.read()?.orientation) { - case Orientation.portrait: - return 1 / 5; - case Orientation.landscape: - return 1 / 8; - case null: - return null; - } + final isPortrait = MediaQuery.orientationOf(context) == Orientation.portrait; + return isPortrait ? 1 / 5 : 1 / 8; } static ScaleState _vectorScaleStateCycle(ScaleState actual) { diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index 40704ae49..0386b9128 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -8,7 +8,7 @@ import 'package:aves/model/settings/enums/entry_background.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; -import 'package:aves/widgets/viewer/visual/state.dart'; +import 'package:aves/model/view_state.dart'; import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/viewer/visual/vector.dart b/lib/widgets/viewer/visual/vector.dart index 5a198cc43..c1502a7d9 100644 --- a/lib/widgets/viewer/visual/vector.dart +++ b/lib/widgets/viewer/visual/vector.dart @@ -1,5 +1,4 @@ import 'dart:math'; -import 'dart:ui'; import 'package:aves/image_providers/region_provider.dart'; import 'package:aves/model/entry/entry.dart'; @@ -9,7 +8,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; -import 'package:aves/widgets/viewer/visual/state.dart'; +import 'package:aves/model/view_state.dart'; import 'package:aves_model/aves_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -103,12 +102,18 @@ class _VectorImageViewState extends State { Widget build(BuildContext context) { if (_displaySize == Size.zero) return widget.errorBuilder(context, 'Not sized', null); + final devicePixelRatio = View.of(context).devicePixelRatio; return ValueListenableBuilder( valueListenable: viewStateNotifier, builder: (context, viewState, child) { final viewportSize = viewState.viewportSize; final viewportSized = viewportSize?.isEmpty == false; - if (viewportSized && !_isTilingInitialized) _initTiling(viewportSize!); + if (viewportSized && !_isTilingInitialized) { + _initTiling( + viewportSize: viewportSize!, + devicePixelRatio: devicePixelRatio, + ); + } return SizedBox.fromSize( size: _displaySize * viewState.scale!, @@ -116,7 +121,7 @@ class _VectorImageViewState extends State { alignment: Alignment.center, children: [ _buildLoading(), - ..._getTiles(), + ..._getTiles(devicePixelRatio), ], ), ); @@ -124,11 +129,14 @@ class _VectorImageViewState extends State { ); } - void _initTiling(Size viewportSize) { + void _initTiling({ + required Size viewportSize, + required double devicePixelRatio, + }) { _tileSide = _displaySize.longestSide; // scale for initial state `contained` final containedScale = min(viewportSize.width / _displaySize.width, viewportSize.height / _displaySize.height); - _minScale = _imageScaleForViewScale(containedScale); + _minScale = _imageScaleForViewScale(scale: containedScale, devicePixelRatio: devicePixelRatio); _isTilingInitialized = true; _registerFullImage(); @@ -154,7 +162,7 @@ class _VectorImageViewState extends State { ); } - List _getTiles() { + List _getTiles(double devicePixelRatio) { if (!_isTilingInitialized) return []; final displayWidth = _displaySize.width; @@ -205,7 +213,7 @@ class _VectorImageViewState extends State { ); final tiles = [fullImageRegionTile]; - final maxSvgScale = max(_imageScaleForViewScale(viewScale), _minScale); + final maxSvgScale = max(_imageScaleForViewScale(scale: viewScale, devicePixelRatio: devicePixelRatio), _minScale); double nextScale(double scale) => scale * 2; // add `alpha` to the region side so that tiles do not align across layers, // which helps the checkered background deflation workaround @@ -273,7 +281,11 @@ class _VectorImageViewState extends State { return Tuple2>(tileRect, regionRect); } - double _imageScaleForViewScale(double scale) => smallestPowerOf2(scale * window.devicePixelRatio).toDouble(); + double _imageScaleForViewScale({ + required double scale, + required double devicePixelRatio, + }) => + smallestPowerOf2(scale * devicePixelRatio).toDouble(); } typedef _BackgroundFrameBuilder = Widget Function(Widget child, int? frame, Rect tileRect); diff --git a/lib/widgets/viewer/visual/video/cover.dart b/lib/widgets/viewer/visual/video/cover.dart index 6091cbd69..5017a4a3f 100644 --- a/lib/widgets/viewer/visual/video/cover.dart +++ b/lib/widgets/viewer/visual/video/cover.dart @@ -115,7 +115,7 @@ class _VideoCoverState extends State { if (boundaries != null) { magnifierController.setScaleBoundaries( boundaries.copyWith( - childSize: videoDisplaySize, + contentSize: videoDisplaySize, ), ); } @@ -144,6 +144,7 @@ class _VideoCoverState extends State { child: ThumbnailImage( entry: entry, extent: extent, + devicePixelRatio: MediaQuery.devicePixelRatioOf(context), fit: BoxFit.contain, showLoadingBackground: false, ), diff --git a/lib/widgets/viewer/visual/video/subtitle/ass_parser.dart b/lib/widgets/viewer/visual/video/subtitle/ass_parser.dart index e6b78b844..06bba15df 100644 --- a/lib/widgets/viewer/visual/video/subtitle/ass_parser.dart +++ b/lib/widgets/viewer/visual/video/subtitle/ass_parser.dart @@ -88,21 +88,18 @@ class AssParser { borderColor: extraStyle.borderColor?.withAlpha(a), ); } - break; } case '1a': { // \1a: fill alpha final a = _parseAlpha(param); if (a != null) textStyle = textStyle.copyWith(color: textStyle.color?.withAlpha(a)); - break; } case '3a': { // \3a: border alpha final a = _parseAlpha(param); if (a != null) extraStyle = extraStyle.copyWith(borderColor: extraStyle.borderColor?.withAlpha(a)); - break; } case '4a': { @@ -118,43 +115,36 @@ class AssParser { )) .toList()); } - break; } case 'a': // \a: line alignment (legacy) extraStyle = _copyWithAlignment(_parseLegacyAlignment(param), extraStyle); - break; case 'an': // \an: line alignment extraStyle = _copyWithAlignment(_parseNewAlignment(param), extraStyle); - break; case 'b': { // \b: bold final weight = _parseFontWeight(param); if (weight != null) textStyle = textStyle.copyWith(fontWeight: weight); - break; } case 'be': { // \be: blurs the edges of the text final times = int.tryParse(param); if (times != null) extraStyle = extraStyle.copyWith(edgeBlur: times == 0 ? 0 : 1); - break; } case 'blur': { // \blur: blurs the edges of the text (Gaussian kernel) final strength = double.tryParse(param); if (strength != null) extraStyle = extraStyle.copyWith(edgeBlur: strength / 2); - break; } case 'bord': { // \bord: border width final size = double.tryParse(param); if (size != null) extraStyle = extraStyle.copyWith(borderWidth: size); - break; } case 'c': case '1c': @@ -164,7 +154,6 @@ class AssParser { if (color != null) { textStyle = textStyle.copyWith(color: color.withAlpha(textStyle.color?.alpha ?? 0xFF)); } - break; } case '3c': { @@ -175,7 +164,6 @@ class AssParser { borderColor: color.withAlpha(extraStyle.borderColor?.alpha ?? 0xFF), ); } - break; } case '4c': { @@ -191,12 +179,10 @@ class AssParser { )) .toList()); } - break; } case 'clip': // \clip: clip (within rectangle or path) line = line.copyWith(clip: _parseClip(param)); - break; case 'fax': { final factor = double.tryParse(param); @@ -204,7 +190,6 @@ class AssParser { if (factor != null && (line.position == null || extraStyle.shearX == null)) { extraStyle = extraStyle.copyWith(shearX: factor); } - break; } case 'fay': { @@ -213,28 +198,24 @@ class AssParser { if (factor != null && (line.position == null || extraStyle.shearY == null)) { extraStyle = extraStyle.copyWith(shearY: factor); } - break; } case 'fn': { final name = param; // TODO TLAD [subtitles] extract fonts from attachment streams, and load these fonts in Flutter if (name.isNotEmpty) textStyle = textStyle.copyWith(fontFamily: name); - break; } case 'frx': { // \frx: text rotation (X axis) final amount = double.tryParse(param); if (amount != null) extraStyle = extraStyle.copyWith(rotationX: amount); - break; } case 'fry': { // \fry: text rotation (Y axis) final amount = double.tryParse(param); if (amount != null) extraStyle = extraStyle.copyWith(rotationY: amount); - break; } case 'fr': case 'frz': @@ -242,40 +223,34 @@ class AssParser { // \frz: text rotation (Z axis) final amount = double.tryParse(param); if (amount != null) extraStyle = extraStyle.copyWith(rotationZ: amount); - break; } case 'fs': { // \fs: font size final size = int.tryParse(param); if (size != null) textStyle = textStyle.copyWith(fontSize: size * scale); - break; } case 'fscx': { // \fscx: font scale (horizontal) final scale = int.tryParse(param); if (scale != null) extraStyle = extraStyle.copyWith(scaleX: scale.toDouble() / 100); - break; } case 'fscy': { // \fscx: font scale (vertical) final scale = int.tryParse(param); if (scale != null) extraStyle = extraStyle.copyWith(scaleY: scale.toDouble() / 100); - break; } case 'fsp': { // \fsp: letter spacing final spacing = double.tryParse(param); textStyle = textStyle.copyWith(letterSpacing: spacing); - break; } case 'i': // \i: italics textStyle = textStyle.copyWith(fontStyle: param == '1' ? FontStyle.italic : FontStyle.normal); - break; case 'p': { // \p drawing paths @@ -290,7 +265,6 @@ class AssParser { extraStyle = extraStyle.copyWith(drawingPaths: null); } } - break; } case 'pos': { @@ -309,20 +283,16 @@ class AssParser { } } } - break; } case 'r': // \r: reset textStyle = baseStyle; - break; case 's': // \s: strikeout textStyle = textStyle.copyWith(decoration: param == '1' ? TextDecoration.lineThrough : TextDecoration.none); - break; case 'u': // \u: underline textStyle = textStyle.copyWith(decoration: param == '1' ? TextDecoration.underline : TextDecoration.none); - break; // TODO TLAD [subtitles] SHOULD support the following case 'shad': case 't': // \t: animated transform @@ -512,13 +482,11 @@ class AssParser { path!.cubicTo(points[0], points[1], points[2], points[3], points[4], points[5]); } } - break; case 'c': if (path != null) { path!.close(); } path = null; - break; case 'l': if (path != null) { const lParamCount = 2; @@ -528,7 +496,6 @@ class AssParser { path!.lineTo(points[0], points[1]); } } - break; case 'm': if (params.length == 2) { if (path != null) { @@ -538,16 +505,13 @@ class AssParser { paths.add(path!); path!.moveTo(params[0], params[1]); } - break; case 'n': if (params.length == 2 && path != null) { path!.moveTo(params[0], params[1]); } - break; case 's': case 'p': debugPrint('unhandled ASS drawing command=$command'); - break; } } }); diff --git a/lib/widgets/viewer/visual/video/subtitle/subtitle.dart b/lib/widgets/viewer/visual/video/subtitle/subtitle.dart index 48919273c..131012944 100644 --- a/lib/widgets/viewer/visual/video/subtitle/subtitle.dart +++ b/lib/widgets/viewer/visual/video/subtitle/subtitle.dart @@ -2,13 +2,13 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/settings/enums/subtitle_position.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/view_state.dart'; import 'package:aves/widgets/common/basic/text/background_painter.dart'; import 'package:aves/widgets/common/basic/text/outlined.dart'; -import 'package:aves_video/aves_video.dart'; -import 'package:aves/widgets/viewer/visual/state.dart'; import 'package:aves/widgets/viewer/visual/video/subtitle/ass_parser.dart'; import 'package:aves/widgets/viewer/visual/video/subtitle/span.dart'; import 'package:aves/widgets/viewer/visual/video/subtitle/style.dart'; +import 'package:aves_video/aves_video.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -53,234 +53,217 @@ class VideoSubtitles extends StatelessWidget { shadows: settings.subtitleShowOutline ? baseShadows : null, ); - return Selector( - selector: (context, mq) => mq.orientation, - builder: (context, orientation, child) { - final bottom = orientation == Orientation.portrait ? .5 : .8; - final viewportSize = context.read().size; + final viewportSize = MediaQuery.sizeOf(context); + final isPortrait = MediaQuery.orientationOf(context) == Orientation.portrait; + final bottom = isPortrait ? .5 : .8; + return ValueListenableBuilder( + valueListenable: viewStateNotifier, + builder: (context, viewState, child) { + final viewPosition = viewState.position; + final viewScale = viewState.scale ?? 1; + final viewSize = videoDisplaySize * viewScale; + final viewOffset = Offset( + (viewportSize.width - viewSize.width) / 2, + (viewportSize.height - viewSize.height) / 2, + ); - return ValueListenableBuilder( - valueListenable: viewStateNotifier, - builder: (context, viewState, child) { - final viewPosition = viewState.position; - final viewScale = viewState.scale ?? 1; - final viewSize = videoDisplaySize * viewScale; - final viewOffset = Offset( - (viewportSize.width - viewSize.width) / 2, - (viewportSize.height - viewSize.height) / 2, - ); + return StreamBuilder( + stream: controller.timedTextStream, + builder: (context, snapshot) { + final text = snapshot.data; + if (text == null) return const SizedBox(); - return StreamBuilder( - stream: controller.timedTextStream, - builder: (context, snapshot) { - final text = snapshot.data; - if (text == null) return const SizedBox(); + if (debugMode) { + return Padding( + padding: const EdgeInsets.only(top: 100.0), + child: Align( + alignment: Alignment.topLeft, + child: OutlinedText( + textSpans: [ + TextSpan( + text: text, + style: const TextStyle(fontSize: 14), + ) + ], + outlineWidth: 1, + outlineColor: Colors.black, + ), + ), + ); + } - if (debugMode) { - return Padding( - padding: const EdgeInsets.only(top: 100.0), + final styledLine = AssParser.parse(text, baseStyle, viewScale); + final position = styledLine.position; + final clip = styledLine.clip; + final styledSpans = styledLine.spans; + final byExtraStyle = groupBy(styledSpans, (v) => v.extraStyle); + return Stack( + children: byExtraStyle.entries.map((kv) { + final extraStyle = kv.key; + final spans = kv.value.map((v) { + final span = v.textSpan; + final style = span.style; + if (position == null || style == null) return span; + + final letterSpacing = style.letterSpacing; + final shadows = style.shadows; + return TextSpan( + text: span.text, + style: style.copyWith( + letterSpacing: letterSpacing != null ? letterSpacing * viewScale : null, + shadows: shadows + ?.map((v) => Shadow( + color: v.color, + offset: v.offset * viewScale, + blurRadius: v.blurRadius * viewScale, + )) + .toList(), + ), + ); + }).toList(); + final drawingPaths = extraStyle.drawingPaths; + final textHAlign = extraStyle.hAlign ?? (position != null ? TextAlign.center : baseTextAlign); + final textVAlign = extraStyle.vAlign ?? (position != null ? TextAlignVertical.bottom : baseTextAlignY); + + Widget child; + if (drawingPaths != null) { + child = CustomPaint( + painter: SubtitlePathPainter( + paths: drawingPaths, + scale: viewScale, + fillColor: spans.firstOrNull?.style?.color ?? Colors.white, + strokeColor: extraStyle.borderColor, + ), + ); + } else { + final outlineWidth = extraStyle.borderWidth ?? (extraStyle.edgeBlur != null ? 2 : 1); + child = OutlinedText( + textSpans: spans, + outlineWidth: outlineWidth * (position != null ? viewScale : baseOutlineWidth), + outlineColor: extraStyle.borderColor ?? baseOutlineColor, + outlineBlurSigma: extraStyle.edgeBlur ?? 0, + textAlign: textHAlign, + ); + } + + var transform = Matrix4.identity(); + + if (position != null) { + final para = RenderParagraph( + TextSpan(children: spans), + textDirection: TextDirection.ltr, + textScaleFactor: MediaQuery.textScaleFactorOf(context), + )..layout(const BoxConstraints()); + final textWidth = para.getMaxIntrinsicWidth(double.infinity); + final textHeight = para.getMaxIntrinsicHeight(double.infinity); + + late double anchorOffsetX, anchorOffsetY; + switch (textHAlign) { + case TextAlign.left: + anchorOffsetX = 0; + case TextAlign.right: + anchorOffsetX = -textWidth; + case TextAlign.center: + default: + anchorOffsetX = -textWidth / 2; + } + switch (textVAlign) { + case TextAlignVertical.top: + anchorOffsetY = 0; + case TextAlignVertical.center: + anchorOffsetY = -textHeight / 2; + case TextAlignVertical.bottom: + anchorOffsetY = -textHeight; + } + final alignOffset = Offset(anchorOffsetX, anchorOffsetY); + final lineOffset = position * viewScale + viewPosition; + final translateOffset = viewOffset + lineOffset + alignOffset; + transform.translate(translateOffset.dx, translateOffset.dy); + } + + if (extraStyle.rotating) { + // for perspective + transform.setEntry(3, 2, 0.001); + final x = -angles.degToRadian(extraStyle.rotationX ?? 0); + final y = -angles.degToRadian(extraStyle.rotationY ?? 0); + final z = -angles.degToRadian(extraStyle.rotationZ ?? 0); + if (x != 0) transform.rotateX(x); + if (y != 0) transform.rotateY(y); + if (z != 0) transform.rotateZ(z); + } + if (extraStyle.scaling) { + final x = extraStyle.scaleX ?? 1; + final y = extraStyle.scaleY ?? 1; + transform.scale(x, y); + } + if (extraStyle.shearing) { + final x = extraStyle.shearX ?? 0; + final y = extraStyle.shearY ?? 0; + transform.multiply(Matrix4(1, y, 0, 0, x, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)); + } + + if (!transform.isIdentity()) { + child = Transform( + transform: transform, + alignment: Alignment.center, + child: child, + ); + } + + if (position == null) { + late double alignX; + switch (textHAlign) { + case TextAlign.left: + alignX = -1; + case TextAlign.right: + alignX = 1; + case TextAlign.center: + default: + alignX = 0; + } + late double alignY; + switch (textVAlign) { + case TextAlignVertical.top: + alignY = -bottom; + case TextAlignVertical.center: + alignY = 0; + case TextAlignVertical.bottom: + default: + alignY = bottom; + } + child = Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), child: Align( - alignment: Alignment.topLeft, - child: OutlinedText( - textSpans: [ - TextSpan( - text: text, - style: const TextStyle(fontSize: 14), - ) - ], - outlineWidth: 1, - outlineColor: Colors.black, + alignment: Alignment(alignX, alignY), + child: TextBackgroundPainter( + spans: spans, + style: DefaultTextStyle.of(context).style.merge(spans.first.style!.copyWith( + backgroundColor: settings.subtitleBackgroundColor, + )), + textAlign: textHAlign, + child: child, ), ), ); } - final styledLine = AssParser.parse(text, baseStyle, viewScale); - final position = styledLine.position; - final clip = styledLine.clip; - final styledSpans = styledLine.spans; - final byExtraStyle = groupBy(styledSpans, (v) => v.extraStyle); - return Stack( - children: byExtraStyle.entries.map((kv) { - final extraStyle = kv.key; - final spans = kv.value.map((v) { - final span = v.textSpan; - final style = span.style; - if (position == null || style == null) return span; + if (clip != null) { + final clipOffset = viewOffset + viewPosition; + final matrix = Matrix4.identity() + ..translate(clipOffset.dx, clipOffset.dy) + ..scale(viewScale, viewScale); + final transform = matrix.storage; + child = ClipPath( + clipper: SubtitlePathClipper( + paths: clip.map((v) => v.transform(transform)).toList(), + scale: viewScale, + ), + child: child, + ); + } - final letterSpacing = style.letterSpacing; - final shadows = style.shadows; - return TextSpan( - text: span.text, - style: style.copyWith( - letterSpacing: letterSpacing != null ? letterSpacing * viewScale : null, - shadows: shadows - ?.map((v) => Shadow( - color: v.color, - offset: v.offset * viewScale, - blurRadius: v.blurRadius * viewScale, - )) - .toList(), - ), - ); - }).toList(); - final drawingPaths = extraStyle.drawingPaths; - final textHAlign = extraStyle.hAlign ?? (position != null ? TextAlign.center : baseTextAlign); - final textVAlign = extraStyle.vAlign ?? (position != null ? TextAlignVertical.bottom : baseTextAlignY); - - Widget child; - if (drawingPaths != null) { - child = CustomPaint( - painter: SubtitlePathPainter( - paths: drawingPaths, - scale: viewScale, - fillColor: spans.firstOrNull?.style?.color ?? Colors.white, - strokeColor: extraStyle.borderColor, - ), - ); - } else { - final outlineWidth = extraStyle.borderWidth ?? (extraStyle.edgeBlur != null ? 2 : 1); - child = OutlinedText( - textSpans: spans, - outlineWidth: outlineWidth * (position != null ? viewScale : baseOutlineWidth), - outlineColor: extraStyle.borderColor ?? baseOutlineColor, - outlineBlurSigma: extraStyle.edgeBlur ?? 0, - textAlign: textHAlign, - ); - } - - var transform = Matrix4.identity(); - - if (position != null) { - final para = RenderParagraph( - TextSpan(children: spans), - textDirection: TextDirection.ltr, - textScaleFactor: context.read().textScaleFactor, - )..layout(const BoxConstraints()); - final textWidth = para.getMaxIntrinsicWidth(double.infinity); - final textHeight = para.getMaxIntrinsicHeight(double.infinity); - - late double anchorOffsetX, anchorOffsetY; - switch (textHAlign) { - case TextAlign.left: - anchorOffsetX = 0; - break; - case TextAlign.right: - anchorOffsetX = -textWidth; - break; - case TextAlign.center: - default: - anchorOffsetX = -textWidth / 2; - break; - } - switch (textVAlign) { - case TextAlignVertical.top: - anchorOffsetY = 0; - break; - case TextAlignVertical.center: - anchorOffsetY = -textHeight / 2; - break; - case TextAlignVertical.bottom: - anchorOffsetY = -textHeight; - break; - } - final alignOffset = Offset(anchorOffsetX, anchorOffsetY); - final lineOffset = position * viewScale + viewPosition; - final translateOffset = viewOffset + lineOffset + alignOffset; - transform.translate(translateOffset.dx, translateOffset.dy); - } - - if (extraStyle.rotating) { - // for perspective - transform.setEntry(3, 2, 0.001); - final x = -angles.degToRadian(extraStyle.rotationX ?? 0); - final y = -angles.degToRadian(extraStyle.rotationY ?? 0); - final z = -angles.degToRadian(extraStyle.rotationZ ?? 0); - if (x != 0) transform.rotateX(x); - if (y != 0) transform.rotateY(y); - if (z != 0) transform.rotateZ(z); - } - if (extraStyle.scaling) { - final x = extraStyle.scaleX ?? 1; - final y = extraStyle.scaleY ?? 1; - transform.scale(x, y); - } - if (extraStyle.shearing) { - final x = extraStyle.shearX ?? 0; - final y = extraStyle.shearY ?? 0; - transform.multiply(Matrix4(1, y, 0, 0, x, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)); - } - - if (!transform.isIdentity()) { - child = Transform( - transform: transform, - alignment: Alignment.center, - child: child, - ); - } - - if (position == null) { - late double alignX; - switch (textHAlign) { - case TextAlign.left: - alignX = -1; - break; - case TextAlign.right: - alignX = 1; - break; - case TextAlign.center: - default: - alignX = 0; - break; - } - late double alignY; - switch (textVAlign) { - case TextAlignVertical.top: - alignY = -bottom; - break; - case TextAlignVertical.center: - alignY = 0; - break; - case TextAlignVertical.bottom: - default: - alignY = bottom; - break; - } - child = Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Align( - alignment: Alignment(alignX, alignY), - child: TextBackgroundPainter( - spans: spans, - style: DefaultTextStyle.of(context).style.merge(spans.first.style!.copyWith( - backgroundColor: settings.subtitleBackgroundColor, - )), - textAlign: textHAlign, - child: child, - ), - ), - ); - } - - if (clip != null) { - final clipOffset = viewOffset + viewPosition; - final matrix = Matrix4.identity() - ..translate(clipOffset.dx, clipOffset.dy) - ..scale(viewScale, viewScale); - final transform = matrix.storage; - child = ClipPath( - clipper: SubtitlePathClipper( - paths: clip.map((v) => v.transform(transform)).toList(), - scale: viewScale, - ), - child: child, - ); - } - - return child; - }).toList(), - ); - }, + return child; + }).toList(), ); }, ); diff --git a/lib/widgets/viewer/visual/video/swipe_action.dart b/lib/widgets/viewer/visual/video/swipe_action.dart index a921cca38..30b049672 100644 --- a/lib/widgets/viewer/visual/video/swipe_action.dart +++ b/lib/widgets/viewer/visual/video/swipe_action.dart @@ -22,10 +22,8 @@ extension ExtraSwipeAction on SwipeAction { switch (this) { case SwipeAction.brightness: await ScreenBrightness().setScreenBrightness(value); - break; case SwipeAction.volume: VolumeController().setVolume(value, showSystemUI: false); - break; } } } diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index a06e1f902..9a03e5a52 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -59,7 +59,7 @@ class _WelcomePageState extends State { if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return const SizedBox(); final terms = snapshot.data!; final durations = context.watch(); - final isPortrait = context.select((mq) => mq.orientation) == Orientation.portrait; + final isPortrait = MediaQuery.orientationOf(context) == Orientation.portrait; return Column( mainAxisSize: MainAxisSize.min, children: _toStaggeredList( diff --git a/plugins/aves_magnifier/lib/aves_magnifier.dart b/plugins/aves_magnifier/lib/aves_magnifier.dart index dae50972d..7a387be0a 100644 --- a/plugins/aves_magnifier/lib/aves_magnifier.dart +++ b/plugins/aves_magnifier/lib/aves_magnifier.dart @@ -2,8 +2,8 @@ library aves_magnifier; export 'src/controller/controller.dart'; export 'src/controller/state.dart'; +export 'src/core/core.dart'; export 'src/core/scale_gesture_recognizer.dart'; -export 'src/magnifier.dart'; export 'src/pan/gesture_detector_scope.dart'; export 'src/pan/scroll_physics.dart'; export 'src/scale/scale_boundaries.dart'; diff --git a/plugins/aves_magnifier/lib/src/controller/controller.dart b/plugins/aves_magnifier/lib/src/controller/controller.dart index 89858643f..e57d35c10 100644 --- a/plugins/aves_magnifier/lib/src/controller/controller.dart +++ b/plugins/aves_magnifier/lib/src/controller/controller.dart @@ -116,17 +116,15 @@ class AvesMagnifierController { final boundaries = scaleBoundaries; if (boundaries == null) return null; - double _clamp(double scale) => scale.clamp(boundaries.minScale, boundaries.maxScale); - switch (scaleState) { case ScaleState.initial: case ScaleState.zoomedIn: case ScaleState.zoomedOut: - return _clamp(boundaries.initialScale); + return boundaries.clampScale(boundaries.initialScale); case ScaleState.covering: - return _clamp(ScaleLevel.scaleForCovering(boundaries.viewportSize, boundaries.childSize)); + return boundaries.clampScale(ScaleLevel.scaleForCovering(boundaries.viewportSize, boundaries.contentSize)); case ScaleState.originalSize: - return _clamp(boundaries.originalScale); + return boundaries.clampScale(boundaries.originalScale); default: return null; } diff --git a/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart b/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart index 21b373790..26955113c 100644 --- a/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart +++ b/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart @@ -10,14 +10,14 @@ import 'package:flutter/widgets.dart'; /// A class to hold internal layout logic to sync both controller states /// /// It reacts to layout changes (eg: enter landscape or widget resize) and syncs the two controllers. -mixin AvesMagnifierControllerDelegate on State { +mixin AvesMagnifierControllerDelegate on State { AvesMagnifierController get controller => widget.controller; ScaleBoundaries? get scaleBoundaries => controller.scaleBoundaries; ScaleStateCycle get scaleStateCycle => widget.scaleStateCycle; - Alignment get basePosition => Alignment.center; + Alignment get basePosition => ScaleBoundaries.basePosition; Function(double? prevScale, double? nextScale, Offset nextPosition)? _animateScale; @@ -26,12 +26,12 @@ mixin AvesMagnifierControllerDelegate on State { final List _subscriptions = []; - void registerDelegate(MagnifierCore widget) { + void registerDelegate(AvesMagnifier widget) { _subscriptions.add(widget.controller.stateStream.listen(_onMagnifierStateChange)); _subscriptions.add(widget.controller.scaleStateChangeStream.listen(_onScaleStateChange)); } - void unregisterDelegate(MagnifierCore oldWidget) { + void unregisterDelegate(AvesMagnifier oldWidget) { _animateScale = null; _subscriptions ..forEach((sub) => sub.cancel()) @@ -54,7 +54,7 @@ mixin AvesMagnifierControllerDelegate on State { final childFocalPoint = scaleStateChange.childFocalPoint; final boundaries = scaleBoundaries; if (childFocalPoint != null && boundaries != null) { - nextPosition = boundaries.childToStatePosition(nextScale!, childFocalPoint); + nextPosition = boundaries.contentToStatePosition(nextScale!, childFocalPoint); } } @@ -70,7 +70,7 @@ mixin AvesMagnifierControllerDelegate on State { final boundaries = scaleBoundaries; if (boundaries == null) return; - controller.update(position: clampPosition(), source: state.source); + controller.update(position: boundaries.clampPosition(position: position, scale: scale!), source: state.source); if (controller.scale == controller.previousState.scale) return; if (state.source == ChangeSource.internal || state.source == ChangeSource.animation) return; @@ -100,14 +100,6 @@ mixin AvesMagnifierControllerDelegate on State { void setScale(double? scale, ChangeSource source) => controller.update(scale: scale, source: source); - void updateMultiple({ - required Offset position, - required double scale, - required ChangeSource source, - }) { - controller.update(position: position, scale: scale, source: source); - } - void updateScaleStateFromNewScale(double newScale, ChangeSource source) { final boundaries = scaleBoundaries; if (boundaries == null) return; @@ -142,74 +134,4 @@ mixin AvesMagnifierControllerDelegate on State { if (originalScale == nextScale) return; controller.setScaleState(nextScaleState, source, childFocalPoint: childFocalPoint); } - - EdgeRange getXEdges({double? scale}) { - final boundaries = scaleBoundaries; - if (boundaries == null) return const EdgeRange(0, 0); - - final _scale = scale ?? this.scale!; - - final computedWidth = boundaries.childSize.width * _scale; - final screenWidth = boundaries.viewportSize.width; - - final positionX = basePosition.x; - final widthDiff = computedWidth - screenWidth; - - final minX = ((positionX - 1).abs() / 2) * widthDiff * -1; - final maxX = ((positionX + 1).abs() / 2) * widthDiff; - return EdgeRange(minX, maxX); - } - - EdgeRange getYEdges({double? scale}) { - final boundaries = scaleBoundaries; - if (boundaries == null) return const EdgeRange(0, 0); - - final _scale = scale ?? this.scale!; - - final computedHeight = boundaries.childSize.height * _scale; - final screenHeight = boundaries.viewportSize.height; - - final positionY = basePosition.y; - final heightDiff = computedHeight - screenHeight; - - final minY = ((positionY - 1).abs() / 2) * heightDiff * -1; - final maxY = ((positionY + 1).abs() / 2) * heightDiff; - return EdgeRange(minY, maxY); - } - - Offset clampPosition({Offset? position, double? scale}) { - final boundaries = scaleBoundaries; - if (boundaries == null) return Offset.zero; - - final _scale = scale ?? this.scale!; - final _position = position ?? this.position; - - final computedWidth = boundaries.childSize.width * _scale; - final computedHeight = boundaries.childSize.height * _scale; - - final screenWidth = boundaries.viewportSize.width; - final screenHeight = boundaries.viewportSize.height; - - var finalX = 0.0; - if (screenWidth < computedWidth) { - final range = getXEdges(scale: _scale); - finalX = _position.dx.clamp(range.min, range.max); - } - - var finalY = 0.0; - if (screenHeight < computedHeight) { - final range = getYEdges(scale: _scale); - finalY = _position.dy.clamp(range.min, range.max); - } - - return Offset(finalX, finalY); - } -} - -/// Simple class to store a min and a max value -class EdgeRange { - const EdgeRange(this.min, this.max); - - final double min; - final double max; } diff --git a/plugins/aves_magnifier/lib/src/controller/range.dart b/plugins/aves_magnifier/lib/src/controller/range.dart new file mode 100644 index 000000000..116ceef0d --- /dev/null +++ b/plugins/aves_magnifier/lib/src/controller/range.dart @@ -0,0 +1,15 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; + +@immutable +class EdgeRange extends Equatable { + final double min; + final double max; + + @override + List get props => [min, max]; + + const EdgeRange(this.min, this.max); + + static const EdgeRange zero = EdgeRange(0, 0); +} diff --git a/plugins/aves_magnifier/lib/src/core/core.dart b/plugins/aves_magnifier/lib/src/core/core.dart index de9829584..53cb4da9a 100644 --- a/plugins/aves_magnifier/lib/src/core/core.dart +++ b/plugins/aves_magnifier/lib/src/core/core.dart @@ -4,23 +4,52 @@ import 'package:aves_magnifier/src/controller/controller.dart'; import 'package:aves_magnifier/src/controller/controller_delegate.dart'; import 'package:aves_magnifier/src/controller/state.dart'; import 'package:aves_magnifier/src/core/gesture_detector.dart'; -import 'package:aves_magnifier/src/magnifier.dart'; import 'package:aves_magnifier/src/pan/edge_hit_detector.dart'; import 'package:aves_magnifier/src/scale/scale_boundaries.dart'; +import 'package:aves_magnifier/src/scale/scale_level.dart'; import 'package:aves_magnifier/src/scale/state.dart'; +import 'package:aves_utils/aves_utils.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; -import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; -/// Internal widget in which controls all animations lifecycle, core responses -/// to user gestures, updates to the controller state and mounts the entire Layout -class MagnifierCore extends StatefulWidget { +/* + adapted from package `photo_view` v0.9.2: + - removed image related aspects to focus on a general purpose pan/scale viewer (à la `InteractiveViewer`) + - removed rotation and many customization parameters + - removed ignorable/ignoring partial notifiers + - formatted, renamed and reorganized + - fixed gesture recognizers when used inside a scrollable widget like `PageView` + - fixed corner hit detection when in containers scrollable in both axes + - fixed corner hit detection issues due to imprecise double comparisons + - added single & double tap position feedback + - fixed focus when scaling by double-tap/pinch + */ +class AvesMagnifier extends StatefulWidget { + static const double defaultPanInertia = .2; + final AvesMagnifierController controller; + final EdgeInsets viewportPadding; + + // The size of the custom [child]. This value is used to compute the relation between the child and the container's size to calculate the scale value. + final Size contentSize; + + final bool allowOriginalScaleBeyondRange; + final bool allowGestureScaleBeyondRange; + final double panInertia; + + // Defines the minimum size in which the image will be allowed to assume, it is proportional to the original image size. + final ScaleLevel minScale; + + // Defines the maximum size in which the image will be allowed to assume, it is proportional to the original image size. + final ScaleLevel maxScale; + + // Defines the size the image will assume when the component is initialized, it is proportional to the original image size. + final ScaleLevel initialScale; + final ScaleStateCycle scaleStateCycle; final bool applyScale; - final double panInertia; final MagnifierGestureScaleStartCallback? onScaleStart; final MagnifierGestureScaleUpdateCallback? onScaleUpdate; final MagnifierGestureScaleEndCallback? onScaleEnd; @@ -29,12 +58,19 @@ class MagnifierCore extends StatefulWidget { final MagnifierDoubleTapCallback? onDoubleTap; final Widget child; - const MagnifierCore({ + const AvesMagnifier({ super.key, required this.controller, - required this.scaleStateCycle, - required this.applyScale, - this.panInertia = .2, + required this.contentSize, + this.viewportPadding = EdgeInsets.zero, + this.allowOriginalScaleBeyondRange = true, + this.allowGestureScaleBeyondRange = true, + this.minScale = const ScaleLevel(factor: .0), + this.maxScale = const ScaleLevel(factor: double.infinity), + this.initialScale = const ScaleLevel(ref: ScaleReference.contained), + this.scaleStateCycle = defaultScaleStateCycle, + this.applyScale = true, + this.panInertia = defaultPanInertia, this.onScaleStart, this.onScaleUpdate, this.onScaleEnd, @@ -45,10 +81,10 @@ class MagnifierCore extends StatefulWidget { }); @override - State createState() => _MagnifierCoreState(); + State createState() => _AvesMagnifierState(); } -class _MagnifierCoreState extends State with TickerProviderStateMixin, AvesMagnifierControllerDelegate, EdgeHitDetector { +class _AvesMagnifierState extends State with TickerProviderStateMixin, AvesMagnifierControllerDelegate, EdgeHitDetector { Offset? _startFocalPoint, _lastViewportFocalPosition; double? _startScale, _quickScaleLastY, _quickScaleLastDistance; late bool _dropped, _doubleTap, _quickScaleMoved; @@ -78,13 +114,23 @@ class _MagnifierCoreState extends State with TickerProviderStateM } @override - void didUpdateWidget(covariant MagnifierCore oldWidget) { + void didUpdateWidget(covariant AvesMagnifier oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.controller != widget.controller) { _unregisterWidget(oldWidget); _registerWidget(widget); } + + if (oldWidget.allowOriginalScaleBeyondRange != widget.allowOriginalScaleBeyondRange || oldWidget.minScale != widget.minScale || oldWidget.maxScale != widget.maxScale || oldWidget.initialScale != widget.initialScale || oldWidget.contentSize != widget.contentSize) { + controller.setScaleBoundaries((controller.scaleBoundaries ?? ScaleBoundaries.zero).copyWith( + allowOriginalScaleBeyondRange: widget.allowOriginalScaleBeyondRange, + minScale: widget.minScale, + maxScale: widget.maxScale, + initialScale: widget.initialScale, + contentSize: widget.contentSize.isEmpty == false ? widget.contentSize : null, + )); + } } @override @@ -95,13 +141,13 @@ class _MagnifierCoreState extends State with TickerProviderStateM super.dispose(); } - void _registerWidget(MagnifierCore widget) { + void _registerWidget(AvesMagnifier widget) { registerDelegate(widget); cachedScaleBoundaries = widget.controller.scaleBoundaries; setScaleStateUpdateAnimation(animateOnScaleStateUpdate); } - void _unregisterWidget(MagnifierCore oldWidget) { + void _unregisterWidget(AvesMagnifier oldWidget) { unregisterDelegate(oldWidget); cachedScaleBoundaries = null; } @@ -171,18 +217,21 @@ class _MagnifierCoreState extends State with TickerProviderStateM } else { newScale = _startScale! * details.scale; } + if (!widget.allowGestureScaleBeyondRange) { + newScale = boundaries.clampScale(newScale); + } + newScale = max(0, newScale); final scaleFocalPoint = _doubleTap ? _startFocalPoint! : details.localFocalPoint; final panPositionDelta = scaleFocalPoint - _lastViewportFocalPosition!; final scalePositionDelta = boundaries.viewportToStatePosition(controller, scaleFocalPoint) * (scale! / newScale - 1); - final newPosition = position + panPositionDelta + scalePositionDelta; + final newPosition = boundaries.clampPosition( + position: position + panPositionDelta + scalePositionDelta, + scale: newScale, + ); updateScaleStateFromNewScale(newScale, ChangeSource.gesture); - updateMultiple( - scale: max(0, newScale), - position: newPosition, - source: ChangeSource.gesture, - ); + controller.update(position: newPosition, scale: newScale, source: ChangeSource.gesture); _lastViewportFocalPosition = scaleFocalPoint; } @@ -220,32 +269,40 @@ class _MagnifierCoreState extends State with TickerProviderStateM } } - final _position = controller.position; - final _scale = controller.scale!; - final maxScale = boundaries.maxScale; - final minScale = boundaries.minScale; + final currentPosition = controller.position; + final currentScale = controller.scale!; // animate back to min/max scale if gesture yielded a scale exceeding them - if (_scale > maxScale || _scale < minScale) { - final newScale = _scale.clamp(minScale, maxScale); - final newPosition = clampPosition(position: _position * newScale / _scale, scale: newScale); - animateScale(_scale, newScale); - animatePosition(_position, newPosition); + final newScale = boundaries.clampScale(currentScale); + if (currentScale != newScale) { + final newPosition = boundaries.clampPosition( + position: currentPosition * newScale / currentScale, + scale: newScale, + ); + animateScale(currentScale, newScale); + animatePosition(currentPosition, newPosition); return; } // The gesture recognizer triggers a new `onScaleStart` every time a pointer/finger is added or removed. // Following a pinch-to-zoom gesture, a new panning gesture may start if the user does not lift both fingers at the same time, // so we dismiss such panning gestures when it looks like it followed a scaling gesture. - final isPanning = _scale == _startScale && (DateTime.now().difference(_lastScaleGestureDate)).inMilliseconds > 100; + final isPanning = currentScale == _startScale && (DateTime.now().difference(_lastScaleGestureDate)).inMilliseconds > 100; // animate position only when panning without scaling if (isPanning) { - final pps = details.velocity.pixelsPerSecond; + var pps = details.velocity.pixelsPerSecond; if (pps != Offset.zero) { - final newPosition = clampPosition(position: _position + pps * widget.panInertia); - if (_position != newPosition) { - final tween = Tween(begin: _position, end: newPosition); + final externalTransform = boundaries.externalTransform; + if (externalTransform != null) { + pps = Matrix4.inverted(externalTransform).transformOffset(pps); + } + final newPosition = boundaries.clampPosition( + position: currentPosition + pps * widget.panInertia, + scale: currentScale, + ); + if (currentPosition != newPosition) { + final tween = Tween(begin: currentPosition, end: newPosition); const curve = Curves.easeOutCubic; _positionAnimation = tween.animate(CurvedAnimation(parent: _positionAnimationController, curve: curve)); _positionAnimationController @@ -255,7 +312,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM } } - if (_scale != _startScale) { + if (currentScale != _startScale) { _lastScaleGestureDate = DateTime.now(); } } @@ -272,7 +329,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM } bool _isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind, Axis axis) { - final gestureSettings = context.read().gestureSettings; + final gestureSettings = MediaQuery.gestureSettingsOf(context); const minVelocity = kMinFlingVelocity; final minDistance = computeHitSlop(kind, gestureSettings); @@ -308,7 +365,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM final viewportTapPosition = details.localPosition; final viewportSize = boundaries.viewportSize; final alignment = Alignment(viewportTapPosition.dx / viewportSize.width, viewportTapPosition.dy / viewportSize.height); - final childTapPosition = boundaries.viewportToChildPosition(controller, viewportTapPosition); + final childTapPosition = boundaries.viewportToContentPosition(controller, viewportTapPosition); onTap(context, controller.currentState, alignment, childTapPosition); } @@ -325,7 +382,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM if (onDoubleTap(alignment) == true) return; } - final childTapPosition = boundaries.viewportToChildPosition(controller, viewportTapPosition); + final childTapPosition = boundaries.viewportToContentPosition(controller, viewportTapPosition); nextScaleState(ChangeSource.gesture, childFocalPoint: childTapPosition); } @@ -376,8 +433,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM stream: controller.stateStream, initialData: controller.previousState, builder: (context, snapshot) { - final boundaries = scaleBoundaries; - if (!snapshot.hasData || boundaries == null) return const SizedBox(); + if (!snapshot.hasData) return const SizedBox(); final magnifierState = snapshot.data!; final position = magnifierState.position; @@ -385,17 +441,19 @@ class _MagnifierCoreState extends State with TickerProviderStateM Widget child = CustomSingleChildLayout( delegate: _CenterWithOriginalSizeDelegate( - boundaries.childSize, + widget.contentSize, basePosition, applyScale, ), child: widget.child, ); + // `Matrix4.scale` uses dynamic typing and can throw `UnimplementedError` on wrong types + final double effectiveScale = (applyScale ? scale : null) ?? 1.0; child = Transform( transform: Matrix4.identity() ..translate(position.dx, position.dy) - ..scale(applyScale ? scale : 1.0), + ..scale(effectiveScale), alignment: basePosition, child: child, ); @@ -407,7 +465,20 @@ class _MagnifierCoreState extends State with TickerProviderStateM onScaleEnd: onScaleEnd, onTapUp: widget.onTap == null ? null : onTap, onDoubleTap: onDoubleTap, - child: child, + child: Padding( + padding: widget.viewportPadding, + child: LayoutBuilder(builder: (context, constraints) { + controller.setScaleBoundaries((controller.scaleBoundaries ?? ScaleBoundaries.zero).copyWith( + allowOriginalScaleBeyondRange: widget.allowOriginalScaleBeyondRange, + minScale: widget.minScale, + maxScale: widget.maxScale, + initialScale: widget.initialScale, + viewportSize: constraints.biggest, + contentSize: widget.contentSize.isEmpty == false ? widget.contentSize : constraints.biggest, + )); + return child; + }), + ), ); }, ); @@ -452,3 +523,15 @@ class _CenterWithOriginalSizeDelegate extends SingleChildLayoutDelegate with Equ return oldDelegate != this; } } + +typedef MagnifierTapCallback = Function( + BuildContext context, + MagnifierState state, + Alignment alignment, + Offset childTapPosition, +); +typedef MagnifierDoubleTapCallback = bool Function(Alignment alignment); +typedef MagnifierGestureScaleStartCallback = void Function(ScaleStartDetails details, bool doubleTap, ScaleBoundaries boundaries); +typedef MagnifierGestureScaleUpdateCallback = bool Function(ScaleUpdateDetails details); +typedef MagnifierGestureScaleEndCallback = void Function(ScaleEndDetails details); +typedef MagnifierGestureFlingCallback = void Function(AxisDirection direction); diff --git a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart index e18c4283f..eac8c8e18 100644 --- a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart +++ b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart @@ -3,7 +3,6 @@ import 'package:aves_magnifier/src/pan/edge_hit_detector.dart'; import 'package:aves_magnifier/src/pan/gesture_detector_scope.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; -import 'package:provider/provider.dart'; class MagnifierGestureDetector extends StatefulWidget { const MagnifierGestureDetector({ @@ -40,7 +39,7 @@ class _MagnifierGestureDetectorState extends State { @override Widget build(BuildContext context) { - final gestureSettings = context.select((mq) => mq.gestureSettings); + final gestureSettings = MediaQuery.gestureSettingsOf(context); final gestures = {}; if (widget.onTapDown != null || widget.onTapUp != null) { @@ -59,12 +58,12 @@ class _MagnifierGestureDetectorState extends State { gestures[MagnifierGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => MagnifierGestureRecognizer( debugOwner: this, - hitDetector: widget.hitDetector, scope: scope, doubleTapDetails: doubleTapDetails, ), (instance) { instance + ..hitDetector = widget.hitDetector ..onStart = widget.onScaleStart != null ? (details) => widget.onScaleStart!(details, doubleTapDetails.value != null) : null ..onUpdate = widget.onScaleUpdate ..onEnd = widget.onScaleEnd diff --git a/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart b/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart index 85b7cfcae..8fc3186da 100644 --- a/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart +++ b/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart @@ -6,13 +6,13 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; class MagnifierGestureRecognizer extends ScaleGestureRecognizer { - final EdgeHitDetector hitDetector; final MagnifierGestureDetectorScope scope; final ValueNotifier doubleTapDetails; + EdgeHitDetector? hitDetector; + MagnifierGestureRecognizer({ super.debugOwner, - required this.hitDetector, required this.scope, required this.doubleTapDetails, }); @@ -93,10 +93,8 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { // when it should yield to other recognizers final canAccept = _areMultiPointers() || _isPriorityGesture() || _canPanX() || _canPanY(); super.resolve(canAccept ? GestureDisposition.accepted : GestureDisposition.rejected); - break; case GestureDisposition.rejected: super.resolve(disposition); - break; } } @@ -137,9 +135,9 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { return false; } - bool _canPanX() => hitDetector.shouldMoveX(move, scope.escapeByFling) && isXPan(move); + bool _canPanX() => hitDetector != null && hitDetector!.shouldMoveX(move, scope.escapeByFling) && isXPan(move); - bool _canPanY() => hitDetector.shouldMoveY(move, scope.escapeByFling) && isYPan(move); + bool _canPanY() => hitDetector != null && hitDetector!.shouldMoveY(move, scope.escapeByFling) && isYPan(move); bool _isOverSlop(PointerDeviceKind kind) { final spanDelta = (_currentSpan! - _initialSpan!).abs(); diff --git a/plugins/aves_magnifier/lib/src/magnifier.dart b/plugins/aves_magnifier/lib/src/magnifier.dart deleted file mode 100644 index 38f5e1d02..000000000 --- a/plugins/aves_magnifier/lib/src/magnifier.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:aves_magnifier/src/controller/controller.dart'; -import 'package:aves_magnifier/src/controller/state.dart'; -import 'package:aves_magnifier/src/core/core.dart'; -import 'package:aves_magnifier/src/scale/scale_boundaries.dart'; -import 'package:aves_magnifier/src/scale/scale_level.dart'; -import 'package:aves_magnifier/src/scale/state.dart'; -import 'package:flutter/material.dart'; - -/* - adapted from package `photo_view` v0.9.2: - - removed image related aspects to focus on a general purpose pan/scale viewer (à la `InteractiveViewer`) - - removed rotation and many customization parameters - - removed ignorable/ignoring partial notifiers - - formatted, renamed and reorganized - - fixed gesture recognizers when used inside a scrollable widget like `PageView` - - fixed corner hit detection when in containers scrollable in both axes - - fixed corner hit detection issues due to imprecise double comparisons - - added single & double tap position feedback - - fixed focus when scaling by double-tap/pinch - */ -class AvesMagnifier extends StatelessWidget { - const AvesMagnifier({ - super.key, - required this.controller, - required this.childSize, - this.allowOriginalScaleBeyondRange = true, - this.minScale = const ScaleLevel(factor: .0), - this.maxScale = const ScaleLevel(factor: double.infinity), - this.initialScale = const ScaleLevel(ref: ScaleReference.contained), - this.scaleStateCycle = defaultScaleStateCycle, - this.applyScale = true, - this.onScaleStart, - this.onScaleUpdate, - this.onScaleEnd, - this.onFling, - this.onTap, - this.onDoubleTap, - required this.child, - }); - - final AvesMagnifierController controller; - - // The size of the custom [child]. This value is used to compute the relation between the child and the container's size to calculate the scale value. - final Size childSize; - - final bool allowOriginalScaleBeyondRange; - - // Defines the minimum size in which the image will be allowed to assume, it is proportional to the original image size. - final ScaleLevel minScale; - - // Defines the maximum size in which the image will be allowed to assume, it is proportional to the original image size. - final ScaleLevel maxScale; - - // Defines the size the image will assume when the component is initialized, it is proportional to the original image size. - final ScaleLevel initialScale; - - final ScaleStateCycle scaleStateCycle; - final bool applyScale; - final MagnifierGestureScaleStartCallback? onScaleStart; - final MagnifierGestureScaleUpdateCallback? onScaleUpdate; - final MagnifierGestureScaleEndCallback? onScaleEnd; - final MagnifierGestureFlingCallback? onFling; - final MagnifierTapCallback? onTap; - final MagnifierDoubleTapCallback? onDoubleTap; - final Widget child; - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - controller.setScaleBoundaries(ScaleBoundaries( - allowOriginalScaleBeyondRange: allowOriginalScaleBeyondRange, - minScale: minScale, - maxScale: maxScale, - initialScale: initialScale, - viewportSize: constraints.biggest, - childSize: childSize.isEmpty == false ? childSize : constraints.biggest, - )); - - return MagnifierCore( - controller: controller, - scaleStateCycle: scaleStateCycle, - applyScale: applyScale, - onScaleStart: onScaleStart, - onScaleUpdate: onScaleUpdate, - onScaleEnd: onScaleEnd, - onFling: onFling, - onTap: onTap, - onDoubleTap: onDoubleTap, - child: child, - ); - }, - ); - } -} - -typedef MagnifierTapCallback = Function( - BuildContext context, - MagnifierState state, - Alignment alignment, - Offset childTapPosition, -); -typedef MagnifierDoubleTapCallback = bool Function(Alignment alignment); -typedef MagnifierGestureScaleStartCallback = void Function(ScaleStartDetails details, bool doubleTap, ScaleBoundaries boundaries); -typedef MagnifierGestureScaleUpdateCallback = bool Function(ScaleUpdateDetails details); -typedef MagnifierGestureScaleEndCallback = void Function(ScaleEndDetails details); -typedef MagnifierGestureFlingCallback = void Function(AxisDirection direction); diff --git a/plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart b/plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart index 551c7c0a9..418e7d790 100644 --- a/plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart +++ b/plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart @@ -1,38 +1,41 @@ import 'package:aves_magnifier/src/controller/controller_delegate.dart'; +import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; mixin EdgeHitDetector on AvesMagnifierControllerDelegate { - // the child width/height is not accurate for some image size & scale combos + // the content width/height is not accurate for some image size & scale combos // e.g. 3580.0 * 0.1005586592178771 yields 360.0 // but 4764.0 * 0.07556675062972293 yields 360.00000000000006 // so be sure to compare with `precisionErrorTolerance` EdgeHit getXEdgeHit() { - final boundaries = scaleBoundaries; - if (boundaries == null) return const EdgeHit(false, false); + final _boundaries = scaleBoundaries; + final _scale = scale; + if (_boundaries == null || _scale == null) return const EdgeHit(false, false); - final childWidth = boundaries.childSize.width * scale!; - final viewportWidth = boundaries.viewportSize.width; - if (viewportWidth + precisionErrorTolerance >= childWidth) { + final contentWidth = _boundaries.contentSize.width * _scale; + final viewportWidth = _boundaries.viewportSize.width; + if (viewportWidth + precisionErrorTolerance >= contentWidth) { return const EdgeHit(true, true); } final x = -position.dx; - final range = getXEdges(); + final range = _boundaries.getXEdges(scale: _scale); return EdgeHit(x <= range.min, x >= range.max); } EdgeHit getYEdgeHit() { - final boundaries = scaleBoundaries; - if (boundaries == null) return const EdgeHit(false, false); + final _boundaries = scaleBoundaries; + final _scale = scale; + if (_boundaries == null || _scale == null) return const EdgeHit(false, false); - final childHeight = boundaries.childSize.height * scale!; - final viewportHeight = boundaries.viewportSize.height; - if (viewportHeight + precisionErrorTolerance >= childHeight) { + final contentHeight = _boundaries.contentSize.height * _scale; + final viewportHeight = _boundaries.viewportSize.height; + if (viewportHeight + precisionErrorTolerance >= contentHeight) { return const EdgeHit(true, true); } final y = -position.dy; - final range = getYEdges(); + final range = _boundaries.getYEdges(scale: _scale); return EdgeHit(y <= range.min, y >= range.max); } @@ -56,12 +59,16 @@ mixin EdgeHitDetector on AvesMagnifierControllerDelegate { } } -class EdgeHit { - const EdgeHit(this.hasHitMin, this.hasHitMax); - +@immutable +class EdgeHit extends Equatable { final bool hasHitMin; final bool hasHitMax; + @override + List get props => [hasHitMin, hasHitMax]; + + const EdgeHit(this.hasHitMin, this.hasHitMax); + bool get hasHitAny => hasHitMin || hasHitMax; bool get hasHitBoth => hasHitMin && hasHitMax; diff --git a/plugins/aves_magnifier/lib/src/scale/scale_boundaries.dart b/plugins/aves_magnifier/lib/src/scale/scale_boundaries.dart index 150bf45bb..f3c12f8f9 100644 --- a/plugins/aves_magnifier/lib/src/scale/scale_boundaries.dart +++ b/plugins/aves_magnifier/lib/src/scale/scale_boundaries.dart @@ -1,13 +1,13 @@ import 'dart:math'; -import 'dart:ui'; import 'package:aves_magnifier/src/controller/controller.dart'; +import 'package:aves_magnifier/src/controller/range.dart'; import 'package:aves_magnifier/src/scale/scale_level.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; /// Internal class to wrap custom scale boundaries (min, max and initial) -/// Also, stores values regarding the two sizes: the container and the child. +/// Also, stores values regarding the two sizes: the container and the content. @immutable class ScaleBoundaries extends Equatable { final bool _allowOriginalScaleBeyondRange; @@ -15,10 +15,13 @@ class ScaleBoundaries extends Equatable { final ScaleLevel _maxScale; final ScaleLevel _initialScale; final Size viewportSize; - final Size childSize; + final Size contentSize; + final Matrix4? externalTransform; + + static const Alignment basePosition = Alignment.center; @override - List get props => [_allowOriginalScaleBeyondRange, _minScale, _maxScale, _initialScale, viewportSize, childSize]; + List get props => [_allowOriginalScaleBeyondRange, _minScale, _maxScale, _initialScale, viewportSize, contentSize, externalTransform]; const ScaleBoundaries({ required bool allowOriginalScaleBeyondRange, @@ -26,67 +29,142 @@ class ScaleBoundaries extends Equatable { required ScaleLevel maxScale, required ScaleLevel initialScale, required this.viewportSize, - required this.childSize, + required this.contentSize, + this.externalTransform, }) : _allowOriginalScaleBeyondRange = allowOriginalScaleBeyondRange, _minScale = minScale, _maxScale = maxScale, _initialScale = initialScale; + static const ScaleBoundaries zero = ScaleBoundaries( + allowOriginalScaleBeyondRange: true, + minScale: ScaleLevel(factor: .0), + maxScale: ScaleLevel(factor: double.infinity), + initialScale: ScaleLevel(ref: ScaleReference.contained), + viewportSize: Size.zero, + contentSize: Size.zero, + ); + ScaleBoundaries copyWith({ - Size? childSize, + bool? allowOriginalScaleBeyondRange, + ScaleLevel? minScale, + ScaleLevel? maxScale, + ScaleLevel? initialScale, + Size? viewportSize, + Size? contentSize, + Matrix4? externalTransform, }) { return ScaleBoundaries( - allowOriginalScaleBeyondRange: _allowOriginalScaleBeyondRange, - minScale: _minScale, - maxScale: _maxScale, - initialScale: _initialScale, - viewportSize: viewportSize, - childSize: childSize ?? this.childSize, + allowOriginalScaleBeyondRange: allowOriginalScaleBeyondRange ?? _allowOriginalScaleBeyondRange, + minScale: minScale ?? _minScale, + maxScale: maxScale ?? _maxScale, + initialScale: initialScale ?? _initialScale, + viewportSize: viewportSize ?? this.viewportSize, + contentSize: contentSize ?? this.contentSize, + externalTransform: externalTransform ?? this.externalTransform, ); } + Size get _transformedViewportSize { + final matrix = externalTransform; + return matrix != null ? MatrixUtils.transformRect(Matrix4.inverted(matrix), Offset.zero & viewportSize).size : viewportSize; + } + double scaleForLevel(ScaleLevel level) { final factor = level.factor; switch (level.ref) { case ScaleReference.contained: - return factor * ScaleLevel.scaleForContained(viewportSize, childSize); + return factor * ScaleLevel.scaleForContained(viewportSize, contentSize); case ScaleReference.covered: - return factor * ScaleLevel.scaleForCovering(viewportSize, childSize); + return factor * ScaleLevel.scaleForCovering(viewportSize, contentSize); case ScaleReference.absolute: default: return factor; } } - double get originalScale => 1.0 / window.devicePixelRatio; - - double get minScale => { - scaleForLevel(_minScale), - _allowOriginalScaleBeyondRange ? originalScale : double.infinity, - initialScale, - }.fold(double.infinity, min); - - double get maxScale => { - scaleForLevel(_maxScale), - _allowOriginalScaleBeyondRange ? originalScale : double.negativeInfinity, - initialScale, - }.fold(0, max); + double get originalScale { + final view = WidgetsBinding.instance.platformDispatcher.views.firstOrNull; + return 1.0 / (view?.devicePixelRatio ?? 1.0); + } double get initialScale => scaleForLevel(_initialScale); Offset get _viewportCenter => viewportSize.center(Offset.zero); - Offset get _childCenter => childSize.center(Offset.zero); + Offset get _contentCenter => contentSize.center(Offset.zero); Offset viewportToStatePosition(AvesMagnifierController controller, Offset viewportPosition) { return viewportPosition - _viewportCenter - controller.position; } - Offset viewportToChildPosition(AvesMagnifierController controller, Offset viewportPosition) { - return viewportToStatePosition(controller, viewportPosition) / controller.scale! + _childCenter; + Offset viewportToContentPosition(AvesMagnifierController controller, Offset viewportPosition) { + return viewportToStatePosition(controller, viewportPosition) / controller.scale! + _contentCenter; } - Offset childToStatePosition(double scale, Offset childPosition) { - return (_childCenter - childPosition) * scale; + Offset contentToStatePosition(double scale, Offset contentPosition) { + return (_contentCenter - contentPosition) * scale; + } + + EdgeRange getXEdges({required double scale}) { + final computedWidth = contentSize.width * scale; + final viewportWidth = _transformedViewportSize.width; + + final positionX = basePosition.x; + final widthDiff = computedWidth - viewportWidth; + + final minX = ((positionX - 1).abs() / 2) * widthDiff * -1; + final maxX = ((positionX + 1).abs() / 2) * widthDiff; + return EdgeRange(minX, maxX); + } + + EdgeRange getYEdges({required double scale}) { + final computedHeight = contentSize.height * scale; + final viewportHeight = _transformedViewportSize.height; + + final positionY = basePosition.y; + final heightDiff = computedHeight - viewportHeight; + + final minY = ((positionY - 1).abs() / 2) * heightDiff * -1; + final maxY = ((positionY + 1).abs() / 2) * heightDiff; + return EdgeRange(minY, maxY); + } + + double clampScale(double scale) { + final minScale = { + scaleForLevel(_minScale), + _allowOriginalScaleBeyondRange ? originalScale : double.infinity, + initialScale, + }.fold(double.infinity, min); + + final maxScale = { + scaleForLevel(_maxScale), + _allowOriginalScaleBeyondRange ? originalScale : double.negativeInfinity, + initialScale, + }.fold(.0, max); + + return scale.clamp(minScale, maxScale); + } + + Offset clampPosition({required Offset position, required double scale}) { + final computedWidth = contentSize.width * scale; + final computedHeight = contentSize.height * scale; + + final viewportWidth = _transformedViewportSize.width; + final viewportHeight = _transformedViewportSize.height; + + var finalX = 0.0; + if (viewportWidth < computedWidth) { + final range = getXEdges(scale: scale); + finalX = position.dx.clamp(range.min, range.max); + } + + var finalY = 0.0; + if (viewportHeight < computedHeight) { + final range = getYEdges(scale: scale); + finalY = position.dy.clamp(range.min, range.max); + } + + return Offset(finalX, finalY); } } diff --git a/plugins/aves_magnifier/lib/src/scale/scale_level.dart b/plugins/aves_magnifier/lib/src/scale/scale_level.dart index a076181ae..b186684e0 100644 --- a/plugins/aves_magnifier/lib/src/scale/scale_level.dart +++ b/plugins/aves_magnifier/lib/src/scale/scale_level.dart @@ -17,9 +17,9 @@ class ScaleLevel extends Equatable { this.factor = 1.0, }); - static double scaleForContained(Size containerSize, Size childSize) => min(containerSize.width / childSize.width, containerSize.height / childSize.height); + static double scaleForContained(Size viewportSize, Size contentSize) => min(viewportSize.width / contentSize.width, viewportSize.height / contentSize.height); - static double scaleForCovering(Size containerSize, Size childSize) => max(containerSize.width / childSize.width, containerSize.height / childSize.height); + static double scaleForCovering(Size viewportSize, Size contentSize) => max(viewportSize.width / contentSize.width, viewportSize.height / contentSize.height); } enum ScaleReference { absolute, contained, covered } diff --git a/plugins/aves_magnifier/lib/src/scale/state.dart b/plugins/aves_magnifier/lib/src/scale/state.dart index 44f5dd533..6cf44a351 100644 --- a/plugins/aves_magnifier/lib/src/scale/state.dart +++ b/plugins/aves_magnifier/lib/src/scale/state.dart @@ -33,12 +33,9 @@ ScaleState defaultScaleStateCycle(ScaleState actual) { case ScaleState.covering: return ScaleState.originalSize; case ScaleState.originalSize: - return ScaleState.initial; case ScaleState.zoomedIn: case ScaleState.zoomedOut: return ScaleState.initial; - default: - return ScaleState.initial; } } diff --git a/plugins/aves_magnifier/pubspec.lock b/plugins/aves_magnifier/pubspec.lock index 03b594497..7ae3ad656 100644 --- a/plugins/aves_magnifier/pubspec.lock +++ b/plugins/aves_magnifier/pubspec.lock @@ -1,22 +1,29 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + aves_utils: + dependency: "direct main" + description: + path: "../aves_utils" + relative: true + source: path + version: "0.0.1" characters: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" equatable: dependency: "direct main" description: @@ -42,18 +49,18 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" material_color_utilities: dependency: transitive description: @@ -66,10 +73,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" nested: dependency: transitive description: @@ -108,5 +115,5 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=1.16.0" diff --git a/plugins/aves_magnifier/pubspec.yaml b/plugins/aves_magnifier/pubspec.yaml index 14f197fcd..cb6f1a9c7 100644 --- a/plugins/aves_magnifier/pubspec.yaml +++ b/plugins/aves_magnifier/pubspec.yaml @@ -3,11 +3,13 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.6 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter + aves_utils: + path: ../aves_utils equatable: provider: tuple: diff --git a/plugins/aves_map/lib/src/marker/generator.dart b/plugins/aves_map/lib/src/marker/generator.dart index 3efff1ebe..ca536b32f 100644 --- a/plugins/aves_map/lib/src/marker/generator.dart +++ b/plugins/aves_map/lib/src/marker/generator.dart @@ -4,7 +4,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:provider/provider.dart'; // generate bitmap from widget, for Google map class MarkerGeneratorWidget extends StatefulWidget { @@ -35,8 +34,12 @@ class _MarkerGeneratorWidgetState extends State oldWidget) { super.didUpdateWidget(oldWidget); + final devicePixelRatio = View.of(context).devicePixelRatio; widget.markers.forEach((markerWidget) { - final item = getOrCreate(markerWidget.key as T); + final item = _getOrCreate( + markerKey: markerWidget.key as T, + devicePixelRatio: devicePixelRatio, + ); item.globalKey = GlobalKey(); }); _checkNextFrame(); @@ -63,7 +66,7 @@ class _MarkerGeneratorWidgetState extends State((mq) => mq.size.width), 0), + offset: Offset(MediaQuery.sizeOf(context).width, 0), child: Material( type: MaterialType.transparency, child: Stack( @@ -78,11 +81,17 @@ class _MarkerGeneratorWidgetState extends State v.markerKey == markerKey); if (existingItem != null) return existingItem; - final newItem = _MarkerGeneratorItem(markerKey); + final newItem = _MarkerGeneratorItem( + markerKey: markerKey, + devicePixelRatio: devicePixelRatio, + ); _items.add(newItem); return newItem; } @@ -92,10 +101,14 @@ enum MarkerGeneratorItemState { waiting, rendering, done } class _MarkerGeneratorItem { final T markerKey; + final double devicePixelRatio; GlobalKey? globalKey; MarkerGeneratorItemState state = MarkerGeneratorItemState.waiting; - _MarkerGeneratorItem(this.markerKey); + _MarkerGeneratorItem({ + required this.markerKey, + required this.devicePixelRatio, + }); bool get isWaiting => state == MarkerGeneratorItemState.waiting; @@ -107,7 +120,7 @@ class _MarkerGeneratorItem { final boundary = _globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary; if (boundary.hasSize && boundary.size != Size.zero) { try { - final image = await boundary.toImage(pixelRatio: ui.window.devicePixelRatio); + final image = await boundary.toImage(pixelRatio: devicePixelRatio); final byteData = await image.toByteData(format: ui.ImageByteFormat.png); bytes = byteData?.buffer.asUint8List(); } catch (error) { diff --git a/plugins/aves_map/pubspec.lock b/plugins/aves_map/pubspec.lock index 0fa75a49e..b463de358 100644 --- a/plugins/aves_map/pubspec.lock +++ b/plugins/aves_map/pubspec.lock @@ -20,10 +20,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" clock: dependency: transitive description: @@ -36,10 +36,10 @@ packages: dependency: "direct main" description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" custom_rounded_rectangle_border: dependency: "direct main" description: @@ -81,18 +81,18 @@ packages: dependency: "direct main" description: name: flutter_map - sha256: "59dfd14267b691bea55760786b47d3172d47cdcc0d79ff930746a5ad123491b8" + sha256: "52c65a977daae42f9aae6748418dd1535eaf27186e9bac9bf431843082bc75a3" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" http: dependency: transitive description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "0.13.6" http_parser: dependency: transitive description: @@ -113,26 +113,26 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" latlong2: dependency: "direct main" description: name: latlong2 - sha256: "408993a0e3f46e79ce1f129e4cb0386eef6d48dfa6394939ecacfbd7049154ec" + sha256: "08ef7282ba9f76e8495e49e2dc4d653015ac929dce5f92b375a415d30b407ea0" url: "https://pub.dev" source: hosted - version: "0.8.1" + version: "0.8.2" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" lists: dependency: transitive description: @@ -153,10 +153,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mgrs_dart: dependency: transitive description: @@ -189,14 +189,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" - positioned_tap_detector_2: - dependency: transitive - description: - name: positioned_tap_detector_2 - sha256: "52e06863ad3e1f82b058fd05054fc8c9caeeb3b47d5cea7a24bd9320746059c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" proj4dart: dependency: transitive description: @@ -254,10 +246,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" unicode: dependency: transitive description: @@ -283,5 +275,5 @@ packages: source: hosted version: "2.0.0" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_map/pubspec.yaml b/plugins/aves_map/pubspec.yaml index 8f325c421..a6cdca279 100644 --- a/plugins/aves_map/pubspec.yaml +++ b/plugins/aves_map/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.6 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: diff --git a/plugins/aves_model/lib/aves_model.dart b/plugins/aves_model/lib/aves_model.dart index f03a09657..30d67e558 100644 --- a/plugins/aves_model/lib/aves_model.dart +++ b/plugins/aves_model/lib/aves_model.dart @@ -10,6 +10,7 @@ export 'src/actions/move_type.dart'; export 'src/actions/settings.dart'; export 'src/actions/share.dart'; export 'src/actions/slideshow.dart'; +export 'src/editor/enums.dart'; export 'src/entry/base.dart'; export 'src/metadata/enums.dart'; export 'src/metadata/fields.dart'; diff --git a/plugins/aves_model/lib/src/editor/enums.dart b/plugins/aves_model/lib/src/editor/enums.dart new file mode 100644 index 000000000..7d03820ac --- /dev/null +++ b/plugins/aves_model/lib/src/editor/enums.dart @@ -0,0 +1,66 @@ +enum EditorAction { transform } + +enum CropAspectRatio { free, original, square, ar_16_9, ar_4_3 } + +enum TransformActivity { none, pan, resize, straighten } + +enum TransformOrientation { normal, rotate90, rotate180, rotate270, transverse, flipVertical, transpose, flipHorizontal } + +extension ExtraTransformOrientation on TransformOrientation { + TransformOrientation flipHorizontally() { + switch (this) { + case TransformOrientation.normal: + return TransformOrientation.flipHorizontal; + case TransformOrientation.rotate90: + return TransformOrientation.transverse; + case TransformOrientation.rotate180: + return TransformOrientation.flipVertical; + case TransformOrientation.rotate270: + return TransformOrientation.transpose; + case TransformOrientation.transverse: + return TransformOrientation.rotate90; + case TransformOrientation.flipVertical: + return TransformOrientation.rotate180; + case TransformOrientation.transpose: + return TransformOrientation.rotate270; + case TransformOrientation.flipHorizontal: + return TransformOrientation.normal; + } + } + + bool get isFlipped { + switch (this) { + case TransformOrientation.normal: + case TransformOrientation.rotate90: + case TransformOrientation.rotate180: + case TransformOrientation.rotate270: + return false; + case TransformOrientation.transverse: + case TransformOrientation.flipVertical: + case TransformOrientation.transpose: + case TransformOrientation.flipHorizontal: + return true; + } + } + + TransformOrientation rotateClockwise() { + switch (this) { + case TransformOrientation.normal: + return TransformOrientation.rotate90; + case TransformOrientation.rotate90: + return TransformOrientation.rotate180; + case TransformOrientation.rotate180: + return TransformOrientation.rotate270; + case TransformOrientation.rotate270: + return TransformOrientation.normal; + case TransformOrientation.transverse: + return TransformOrientation.flipHorizontal; + case TransformOrientation.flipVertical: + return TransformOrientation.transverse; + case TransformOrientation.transpose: + return TransformOrientation.flipVertical; + case TransformOrientation.flipHorizontal: + return TransformOrientation.transpose; + } + } +} diff --git a/plugins/aves_model/lib/src/settings/enums.dart b/plugins/aves_model/lib/src/settings/enums.dart index f12c6ce7b..3f327feca 100644 --- a/plugins/aves_model/lib/src/settings/enums.dart +++ b/plugins/aves_model/lib/src/settings/enums.dart @@ -14,7 +14,7 @@ enum DisplayRefreshRateMode { auto, highest, lowest } enum EntryBackground { black, white, checkered } -enum HomePageSetting { collection, albums } +enum HomePageSetting { collection, albums, tags } enum KeepScreenOn { never, videoPlayback, viewerOnly, always } @@ -40,10 +40,10 @@ enum VideoLoopMode { never, shortOnly, always } enum VideoResumptionMode { never, ask, always } -enum ViewerTransition { slide, parallax, fade, zoomIn, none } +enum ViewerTransition { slide, parallax, fade, zoomIn, none, random } enum WidgetDisplayedItem { random, mostRecent } -enum WidgetOpenPage { home, collection, viewer } +enum WidgetOpenPage { home, collection, viewer, updateWidget } enum WidgetShape { rrect, circle, heart } diff --git a/plugins/aves_model/pubspec.lock b/plugins/aves_model/pubspec.lock index 206fcaf52..32248f33f 100644 --- a/plugins/aves_model/pubspec.lock +++ b/plugins/aves_model/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" equatable: dependency: "direct main" description: @@ -42,18 +42,18 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" material_color_utilities: dependency: transitive description: @@ -66,10 +66,10 @@ packages: dependency: "direct main" description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" sky_engine: dependency: transitive description: flutter @@ -84,4 +84,4 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0 <4.0.0" diff --git a/plugins/aves_model/pubspec.yaml b/plugins/aves_model/pubspec.yaml index e07e9d4d0..d24556f14 100644 --- a/plugins/aves_model/pubspec.yaml +++ b/plugins/aves_model/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: '>=2.19.6 <3.0.0' + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: diff --git a/plugins/aves_platform_meta/android/build.gradle b/plugins/aves_platform_meta/android/build.gradle index cded37549..757ddd31e 100644 --- a/plugins/aves_platform_meta/android/build.gradle +++ b/plugins/aves_platform_meta/android/build.gradle @@ -2,14 +2,18 @@ group 'deckers.thibault.aves.aves_platform_meta' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.8.0' + ext { + kotlin_version = '1.8.21' + agp_version = '8.0.1' + } + repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath "com.android.tools.build:gradle:$agp_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/plugins/aves_platform_meta/android/gradle/wrapper/gradle-wrapper.properties b/plugins/aves_platform_meta/android/gradle/wrapper/gradle-wrapper.properties index cb92fa5fd..34953c675 100644 --- a/plugins/aves_platform_meta/android/gradle/wrapper/gradle-wrapper.properties +++ b/plugins/aves_platform_meta/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip \ No newline at end of file diff --git a/plugins/aves_platform_meta/android/src/main/AndroidManifest.xml b/plugins/aves_platform_meta/android/src/main/AndroidManifest.xml index 36fa2e6a5..cc947c567 100644 --- a/plugins/aves_platform_meta/android/src/main/AndroidManifest.xml +++ b/plugins/aves_platform_meta/android/src/main/AndroidManifest.xml @@ -1,3 +1 @@ - - + diff --git a/plugins/aves_platform_meta/pubspec.lock b/plugins/aves_platform_meta/pubspec.lock index 10b974b12..d5b51cbb3 100644 --- a/plugins/aves_platform_meta/pubspec.lock +++ b/plugins/aves_platform_meta/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" flutter: dependency: "direct main" description: flutter @@ -34,18 +34,18 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" material_color_utilities: dependency: transitive description: @@ -58,10 +58,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" plugin_platform_interface: dependency: "direct main" description: @@ -84,4 +84,4 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0 <4.0.0" diff --git a/plugins/aves_platform_meta/pubspec.yaml b/plugins/aves_platform_meta/pubspec.yaml index 67620cb1d..c87c30f04 100644 --- a/plugins/aves_platform_meta/pubspec.yaml +++ b/plugins/aves_platform_meta/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.6 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: diff --git a/plugins/aves_report/lib/aves_report.dart b/plugins/aves_report/lib/aves_report.dart index 5c0cf8999..4675cf2b0 100644 --- a/plugins/aves_report/lib/aves_report.dart +++ b/plugins/aves_report/lib/aves_report.dart @@ -1,6 +1,8 @@ library aves_report; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:stack_trace/stack_trace.dart'; abstract class ReportService { Future init(); @@ -18,4 +20,18 @@ abstract class ReportService { Future recordError(dynamic exception, StackTrace? stack); Future recordFlutterError(FlutterErrorDetails flutterErrorDetails); + + static StackTrace? buildReportStack(StackTrace stack, {int level = 0}) { + // simply creating a trace with `Trace.current(1)` or creating a `Trace` from modified frames + // does not yield a stack trace that Crashlytics can segment, + // so we reconstruct a string stack trace instead + return StackTrace.fromString(Trace.from(stack) + .frames + .skip(level) + .toList() + .mapIndexed( + (i, f) => '#${(i++).toString().padRight(8)}${f.member} (${f.uri}:${f.line}:${f.column})', + ) + .join('\n')); + } } diff --git a/plugins/aves_report/pubspec.lock b/plugins/aves_report/pubspec.lock index c656d7419..78b846b96 100644 --- a/plugins/aves_report/pubspec.lock +++ b/plugins/aves_report/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" collection: - dependency: transitive + dependency: "direct main" description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" flutter: dependency: "direct main" description: flutter @@ -34,18 +34,18 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" material_color_utilities: dependency: transitive description: @@ -58,15 +58,31 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + stack_trace: + dependency: "direct main" + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" vector_math: dependency: transitive description: @@ -76,4 +92,4 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0 <4.0.0" diff --git a/plugins/aves_report/pubspec.yaml b/plugins/aves_report/pubspec.yaml index 14de85cf7..ea3090164 100644 --- a/plugins/aves_report/pubspec.yaml +++ b/plugins/aves_report/pubspec.yaml @@ -3,11 +3,13 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.6 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter + collection: + stack_trace: dev_dependencies: flutter_lints: diff --git a/plugins/aves_report_console/pubspec.lock b/plugins/aves_report_console/pubspec.lock index 9be92d272..1a70f8372 100644 --- a/plugins/aves_report_console/pubspec.lock +++ b/plugins/aves_report_console/pubspec.lock @@ -12,18 +12,18 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" flutter: dependency: "direct main" description: flutter @@ -41,18 +41,18 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" material_color_utilities: dependency: transitive description: @@ -65,15 +65,31 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" vector_math: dependency: transitive description: @@ -83,4 +99,4 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0 <4.0.0" diff --git a/plugins/aves_report_console/pubspec.yaml b/plugins/aves_report_console/pubspec.yaml index 93fde45cf..369f1c8b5 100644 --- a/plugins/aves_report_console/pubspec.yaml +++ b/plugins/aves_report_console/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.6 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: diff --git a/plugins/aves_report_crashlytics/lib/aves_report_platform.dart b/plugins/aves_report_crashlytics/lib/aves_report_platform.dart index cc501c381..67fd00aec 100644 --- a/plugins/aves_report_crashlytics/lib/aves_report_platform.dart +++ b/plugins/aves_report_crashlytics/lib/aves_report_platform.dart @@ -3,12 +3,10 @@ library aves_report_platform; import 'dart:async'; import 'package:aves_report/aves_report.dart'; -import 'package:collection/collection.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:stack_trace/stack_trace.dart'; class PlatformReportService extends ReportService { FirebaseCrashlytics? get _instance { @@ -71,17 +69,7 @@ class PlatformReportService extends ReportService { @override Future recordError(dynamic exception, StackTrace? stack) async { if (exception is PlatformException && stack != null) { - // simply creating a trace with `Trace.current(1)` or creating a `Trace` from modified frames - // does not yield a stack trace that Crashlytics can segment, - // so we reconstruct a string stack trace instead - stack = StackTrace.fromString(Trace.from(stack) - .frames - .skip(2) - .toList() - .mapIndexed( - (i, f) => '#${(i++).toString().padRight(8)}${f.member} (${f.uri}:${f.line}:${f.column})', - ) - .join('\n')); + stack = ReportService.buildReportStack(stack, level: 2); } return _instance?.recordError(exception, stack); } diff --git a/plugins/aves_report_crashlytics/pubspec.lock b/plugins/aves_report_crashlytics/pubspec.lock index bd6dde97e..18085d21c 100644 --- a/plugins/aves_report_crashlytics/pubspec.lock +++ b/plugins/aves_report_crashlytics/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "6a0ad72b2bcdb461749e40c01c478212a78db848dfcb2f10f2a461988bc5fb29" + sha256: "8eb354cb8ebed8a9fdf63699d15deff533bc133128898afaf754926b57d611b6" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.3.1" async: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" aves_report: dependency: "direct main" description: @@ -36,10 +36,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" clock: dependency: transitive description: @@ -49,13 +49,13 @@ packages: source: hosted version: "1.1.1" collection: - dependency: "direct main" + dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" fake_async: dependency: transitive description: @@ -68,42 +68,42 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "239e4ac688674a7e7b5476fd16b0d8e2b5a453d464f32091af3ce1df4ebb7316" + sha256: "250678b816279b3240c3a33e1f76bf712c00718f1fbeffc85873a5da8c077379" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.13.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "0df0a064ab0cad7f8836291ca6f3272edd7b83ad5b3540478ee46a0849d8022b" + sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.8.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "347351a8f0518f3343d79a9a0690fa67ad232fc32e2ea270677791949eac792b" + sha256: "8c0f4c87d20e2d001a5915df238c1f9c88704231f591324205f5a5d2a7740a45" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: "02ce958507138d938b4611e3a4910667a44b969b0c20a810bbc97bbf967ba0d7" + sha256: "0d74cca3085f144f99aa4bd82cc4d33280d4cb72bac0b733cbf97c2d7d126df8" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.3.1" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: b9c7b8498c877a2901ad323fc92c10f672be1597bc82d08f121f6228f321a7e0 + sha256: "13880033d5f2055f53bcda28024e16607b8400445a425f86732c1935da9260db" url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.6.1" flutter: dependency: "direct main" description: flutter @@ -131,26 +131,26 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" matcher: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -163,18 +163,18 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" path: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" plugin_platform_interface: dependency: transitive description: @@ -197,7 +197,7 @@ packages: source: hosted version: "1.9.1" stack_trace: - dependency: "direct main" + dependency: transitive description: name: stack_trace sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 @@ -232,10 +232,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" vector_math: dependency: transitive description: @@ -245,5 +245,5 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.6 <3.0.0" - flutter: ">=1.20.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.3.0" diff --git a/plugins/aves_report_crashlytics/pubspec.yaml b/plugins/aves_report_crashlytics/pubspec.yaml index 1740651b8..9186c9e74 100644 --- a/plugins/aves_report_crashlytics/pubspec.yaml +++ b/plugins/aves_report_crashlytics/pubspec.yaml @@ -3,19 +3,17 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.6 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter aves_report: path: ../aves_report - collection: # as of `firebase_core` v2.10.0, upgrading packages downgrades `firebase_core` et al. # so that the transitive `path` gets upgraded to v1.8.3 firebase_core: ">=2.10.0" firebase_crashlytics: - stack_trace: dev_dependencies: flutter_lints: diff --git a/plugins/aves_screen_state/.gitignore b/plugins/aves_screen_state/.gitignore index 96486fd93..28124a571 100644 --- a/plugins/aves_screen_state/.gitignore +++ b/plugins/aves_screen_state/.gitignore @@ -23,7 +23,7 @@ migrate_working_dir/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock +#/pubspec.lock **/doc/api/ .dart_tool/ .packages diff --git a/plugins/aves_screen_state/android/build.gradle b/plugins/aves_screen_state/android/build.gradle index 31eaba649..f8ca69e20 100644 --- a/plugins/aves_screen_state/android/build.gradle +++ b/plugins/aves_screen_state/android/build.gradle @@ -2,14 +2,18 @@ group 'deckers.thibault.aves.aves_screen_state' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.8.0' + ext { + kotlin_version = '1.8.21' + agp_version = '8.0.1' + } + repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath "com.android.tools.build:gradle:$agp_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/plugins/aves_screen_state/android/gradle/wrapper/gradle-wrapper.properties b/plugins/aves_screen_state/android/gradle/wrapper/gradle-wrapper.properties index 3c472b99c..41681a771 100644 --- a/plugins/aves_screen_state/android/gradle/wrapper/gradle-wrapper.properties +++ b/plugins/aves_screen_state/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip \ No newline at end of file diff --git a/plugins/aves_screen_state/android/src/main/AndroidManifest.xml b/plugins/aves_screen_state/android/src/main/AndroidManifest.xml index 0c066880c..cc947c567 100644 --- a/plugins/aves_screen_state/android/src/main/AndroidManifest.xml +++ b/plugins/aves_screen_state/android/src/main/AndroidManifest.xml @@ -1,3 +1 @@ - - + diff --git a/plugins/aves_screen_state/android/src/main/kotlin/deckers/thibault/aves/aves_screen_state/AvesScreenStatePlugin.kt b/plugins/aves_screen_state/android/src/main/kotlin/deckers/thibault/aves/aves_screen_state/AvesScreenStatePlugin.kt index 04f330eca..8beb09d12 100644 --- a/plugins/aves_screen_state/android/src/main/kotlin/deckers/thibault/aves/aves_screen_state/AvesScreenStatePlugin.kt +++ b/plugins/aves_screen_state/android/src/main/kotlin/deckers/thibault/aves/aves_screen_state/AvesScreenStatePlugin.kt @@ -13,13 +13,13 @@ class AvesScreenStatePlugin : FlutterPlugin, EventChannel.StreamHandler { private var context: Context? = null private var screenReceiver: ScreenReceiver? = null - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "deckers.thibault/aves_screen_state/events") context = flutterPluginBinding.applicationContext eventChannel.setStreamHandler(this) } - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { eventChannel.setStreamHandler(null) } diff --git a/plugins/aves_screen_state/pubspec.lock b/plugins/aves_screen_state/pubspec.lock new file mode 100644 index 000000000..d5b51cbb3 --- /dev/null +++ b/plugins/aves_screen_state/pubspec.lock @@ -0,0 +1,87 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + collection: + dependency: transitive + description: + name: collection + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" + source: hosted + version: "1.17.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" + source: hosted + version: "2.0.1" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + lints: + dependency: transitive + description: + name: lints + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: "direct main" + description: + name: plugin_platform_interface + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" +sdks: + dart: ">=3.0.0 <4.0.0" diff --git a/plugins/aves_screen_state/pubspec.yaml b/plugins/aves_screen_state/pubspec.yaml index fda5d6290..9b6570b39 100644 --- a/plugins/aves_screen_state/pubspec.yaml +++ b/plugins/aves_screen_state/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: '>=2.19.6 <3.0.0' + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: diff --git a/plugins/aves_services/pubspec.lock b/plugins/aves_services/pubspec.lock index d6f9acead..0808069d5 100644 --- a/plugins/aves_services/pubspec.lock +++ b/plugins/aves_services/pubspec.lock @@ -27,10 +27,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" clock: dependency: transitive description: @@ -43,10 +43,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" custom_rounded_rectangle_border: dependency: transitive description: @@ -88,18 +88,18 @@ packages: dependency: transitive description: name: flutter_map - sha256: "59dfd14267b691bea55760786b47d3172d47cdcc0d79ff930746a5ad123491b8" + sha256: "52c65a977daae42f9aae6748418dd1535eaf27186e9bac9bf431843082bc75a3" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" http: dependency: transitive description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "0.13.6" http_parser: dependency: transitive description: @@ -120,26 +120,26 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" latlong2: dependency: "direct main" description: name: latlong2 - sha256: "408993a0e3f46e79ce1f129e4cb0386eef6d48dfa6394939ecacfbd7049154ec" + sha256: "08ef7282ba9f76e8495e49e2dc4d653015ac929dce5f92b375a415d30b407ea0" url: "https://pub.dev" source: hosted - version: "0.8.1" + version: "0.8.2" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" lists: dependency: transitive description: @@ -160,10 +160,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mgrs_dart: dependency: transitive description: @@ -196,14 +196,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" - positioned_tap_detector_2: - dependency: transitive - description: - name: positioned_tap_detector_2 - sha256: "52e06863ad3e1f82b058fd05054fc8c9caeeb3b47d5cea7a24bd9320746059c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" proj4dart: dependency: transitive description: @@ -261,10 +253,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" unicode: dependency: transitive description: @@ -290,5 +282,5 @@ packages: source: hosted version: "2.0.0" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_services/pubspec.yaml b/plugins/aves_services/pubspec.yaml index cb818794f..b19e726a8 100644 --- a/plugins/aves_services/pubspec.yaml +++ b/plugins/aves_services/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.6 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: diff --git a/plugins/aves_services_google/lib/src/map.dart b/plugins/aves_services_google/lib/src/map.dart index b3f50548d..d80565c08 100644 --- a/plugins/aves_services_google/lib/src/map.dart +++ b/plugins/aves_services_google/lib/src/map.dart @@ -118,7 +118,6 @@ class _EntryGoogleMapState extends State> with WidgetsBindi // workaround for blank map when resuming app // cf https://github.com/flutter/flutter/issues/40284 _serviceMapController?.setMapStyle(null); - break; } } diff --git a/plugins/aves_services_google/pubspec.lock b/plugins/aves_services_google/pubspec.lock index 5a9c169ee..648872db5 100644 --- a/plugins/aves_services_google/pubspec.lock +++ b/plugins/aves_services_google/pubspec.lock @@ -34,10 +34,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" clock: dependency: transitive description: @@ -50,10 +50,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" custom_rounded_rectangle_border: dependency: transitive description: @@ -66,10 +66,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903 + sha256: "499c61743e13909c13374a8c209075385858c614b9c0f2487b5f9995eeaf7369" url: "https://pub.dev" source: hosted - version: "8.2.2" + version: "9.0.1" device_info_plus_platform_interface: dependency: transitive description: @@ -90,10 +90,10 @@ packages: dependency: transitive description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" file: dependency: transitive description: @@ -127,18 +127,18 @@ packages: dependency: transitive description: name: flutter_map - sha256: "59dfd14267b691bea55760786b47d3172d47cdcc0d79ff930746a5ad123491b8" + sha256: "52c65a977daae42f9aae6748418dd1535eaf27186e9bac9bf431843082bc75a3" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "8ffe990dac54a4a5492747added38571a5ab474c8e5d196809ea08849c69b1bb" + sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.15" flutter_web_plugins: dependency: transitive description: flutter @@ -156,10 +156,10 @@ packages: dependency: transitive description: name: google_api_availability_android - sha256: eb309bc0b435731d18f306b598e176a9afcf642089a7d7c5cbb48e393afda345 + sha256: d95429ae78083585c312de2c6578085e7d53d100a94656d691bce0bb0ce435be url: "https://pub.dev" source: hosted - version: "1.0.0+1" + version: "1.0.1" google_api_availability_platform_interface: dependency: transitive description: @@ -172,42 +172,42 @@ packages: dependency: "direct main" description: name: google_maps_flutter - sha256: "24392ef192f3b00bcd93151375676805a9933574423a5bd5509a0ead2e8a4215" + sha256: abefcb1e5e5c96bdd8084939dda555257af272c7972902ca46d5631092c1df68 url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.2.8" google_maps_flutter_android: dependency: "direct main" description: name: google_maps_flutter_android - sha256: b1aeab571b33e983ced9977f2f0eca2b25925f6319e99e3901cfc1675e1b5ada + sha256: "9512c862df77c1f0fa5f445513dd3c57f5996f0a809dccb74e54b690ee4e3a0f" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" google_maps_flutter_ios: dependency: transitive description: name: google_maps_flutter_ios - sha256: e9ad74415a222573625a2c1717adc1e375b18e8ce660fc12db734d1bda1132d4 + sha256: a9462a433bf3ebe60aadcf4906d2d6341a270d69d3e0fcaa8eb2b64699fcfb4f url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.3" google_maps_flutter_platform_interface: dependency: "direct main" description: name: google_maps_flutter_platform_interface - sha256: a07811d2b82055815ede75e1fe4b7b76f71a0b4820b26f71bdaddd157d6a3e20 + sha256: "308f0af138fa78e8224d598d46ca182673874d0ef4d754b7157c073b5b4b8e0d" url: "https://pub.dev" source: hosted - version: "2.2.6" + version: "2.2.7" http: dependency: transitive description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "0.13.6" http_parser: dependency: transitive description: @@ -228,26 +228,26 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" latlong2: dependency: "direct main" description: name: latlong2 - sha256: "408993a0e3f46e79ce1f129e4cb0386eef6d48dfa6394939ecacfbd7049154ec" + sha256: "08ef7282ba9f76e8495e49e2dc4d653015ac929dce5f92b375a415d30b407ea0" url: "https://pub.dev" source: hosted - version: "0.8.1" + version: "0.8.2" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" lists: dependency: transitive description: @@ -268,10 +268,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mgrs_dart: dependency: transitive description: @@ -312,14 +312,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" - positioned_tap_detector_2: - dependency: transitive - description: - name: positioned_tap_detector_2 - sha256: "52e06863ad3e1f82b058fd05054fc8c9caeeb3b47d5cea7a24bd9320746059c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" proj4dart: dependency: transitive description: @@ -385,10 +377,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" unicode: dependency: transitive description: @@ -409,10 +401,18 @@ packages: dependency: transitive description: name: win32 - sha256: dd8f9344bc305ae2923e3d11a2a911d9a4e2c7dd6fe0ed10626d63211a69676e + sha256: "6ca3aaab1790eeb1f5cad232e33d9c53ba66e884dd3e7686c4e730bffc45f1a3" url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "5.0.2" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9 + url: "https://pub.dev" + source: hosted + version: "1.1.1" wkt_parser: dependency: transitive description: @@ -422,5 +422,5 @@ packages: source: hosted version: "2.0.0" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_services_google/pubspec.yaml b/plugins/aves_services_google/pubspec.yaml index 08a713a60..c072898a4 100644 --- a/plugins/aves_services_google/pubspec.yaml +++ b/plugins/aves_services_google/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.6 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: diff --git a/plugins/aves_services_huawei/pubspec.lock b/plugins/aves_services_huawei/pubspec.lock index 05680f00c..6678d54e8 100644 --- a/plugins/aves_services_huawei/pubspec.lock +++ b/plugins/aves_services_huawei/pubspec.lock @@ -41,10 +41,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" clock: dependency: transitive description: @@ -57,10 +57,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" custom_rounded_rectangle_border: dependency: transitive description: @@ -102,18 +102,18 @@ packages: dependency: transitive description: name: flutter_map - sha256: "59dfd14267b691bea55760786b47d3172d47cdcc0d79ff930746a5ad123491b8" + sha256: "52c65a977daae42f9aae6748418dd1535eaf27186e9bac9bf431843082bc75a3" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" http: dependency: transitive description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "0.13.6" http_parser: dependency: transitive description: @@ -125,18 +125,20 @@ packages: huawei_hmsavailability: dependency: "direct main" description: - name: huawei_hmsavailability - sha256: ab7128852e4188fda1e052cc44d8f881061e79fdd568edd18981347906369653 - url: "https://pub.dev" - source: hosted + path: flutter-hms-availability + ref: agp8-compat + resolved-ref: d861f98fcfbd770420594f92d099ea3128c840c2 + url: "https://github.com/deckerst/hms-flutter-plugin.git" + source: git version: "6.6.0+300" huawei_map: dependency: "direct main" description: - name: huawei_map - sha256: "3cee2a6fe1a8eb03782f29588df082de14b09f81c88b376017ad5afda6df2555" - url: "https://pub.dev" - source: hosted + path: flutter-hms-map + ref: agp8-compat + resolved-ref: d861f98fcfbd770420594f92d099ea3128c840c2 + url: "https://github.com/deckerst/hms-flutter-plugin.git" + source: git version: "6.9.0+300" intl: dependency: transitive @@ -150,26 +152,26 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" latlong2: dependency: "direct main" description: name: latlong2 - sha256: "408993a0e3f46e79ce1f129e4cb0386eef6d48dfa6394939ecacfbd7049154ec" + sha256: "08ef7282ba9f76e8495e49e2dc4d653015ac929dce5f92b375a415d30b407ea0" url: "https://pub.dev" source: hosted - version: "0.8.1" + version: "0.8.2" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" lists: dependency: transitive description: @@ -190,10 +192,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mgrs_dart: dependency: transitive description: @@ -234,14 +236,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" - positioned_tap_detector_2: - dependency: transitive - description: - name: positioned_tap_detector_2 - sha256: "52e06863ad3e1f82b058fd05054fc8c9caeeb3b47d5cea7a24bd9320746059c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" proj4dart: dependency: transitive description: @@ -299,10 +293,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" unicode: dependency: transitive description: @@ -328,5 +322,5 @@ packages: source: hosted version: "2.0.0" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_services_huawei/pubspec.yaml b/plugins/aves_services_huawei/pubspec.yaml index 148a85401..1016be8e0 100644 --- a/plugins/aves_services_huawei/pubspec.yaml +++ b/plugins/aves_services_huawei/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.6 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: @@ -14,8 +14,18 @@ dependencies: path: ../aves_platform_meta aves_services: path: ../aves_services + # cf https://github.com/HMS-Core/hms-flutter-plugin/pull/296 huawei_hmsavailability: + git: + url: https://github.com/deckerst/hms-flutter-plugin.git + ref: agp8-compat + path: flutter-hms-availability + # cf https://github.com/HMS-Core/hms-flutter-plugin/pull/296 huawei_map: + git: + url: https://github.com/deckerst/hms-flutter-plugin.git + ref: agp8-compat + path: flutter-hms-map latlong2: provider: diff --git a/plugins/aves_services_none/pubspec.lock b/plugins/aves_services_none/pubspec.lock index 83aa39d6e..4a659765d 100644 --- a/plugins/aves_services_none/pubspec.lock +++ b/plugins/aves_services_none/pubspec.lock @@ -34,10 +34,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" clock: dependency: transitive description: @@ -50,10 +50,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" custom_rounded_rectangle_border: dependency: transitive description: @@ -95,18 +95,18 @@ packages: dependency: transitive description: name: flutter_map - sha256: "59dfd14267b691bea55760786b47d3172d47cdcc0d79ff930746a5ad123491b8" + sha256: "52c65a977daae42f9aae6748418dd1535eaf27186e9bac9bf431843082bc75a3" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" http: dependency: transitive description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "0.13.6" http_parser: dependency: transitive description: @@ -127,26 +127,26 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" latlong2: dependency: "direct main" description: name: latlong2 - sha256: "408993a0e3f46e79ce1f129e4cb0386eef6d48dfa6394939ecacfbd7049154ec" + sha256: "08ef7282ba9f76e8495e49e2dc4d653015ac929dce5f92b375a415d30b407ea0" url: "https://pub.dev" source: hosted - version: "0.8.1" + version: "0.8.2" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" lists: dependency: transitive description: @@ -167,10 +167,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mgrs_dart: dependency: transitive description: @@ -203,14 +203,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" - positioned_tap_detector_2: - dependency: transitive - description: - name: positioned_tap_detector_2 - sha256: "52e06863ad3e1f82b058fd05054fc8c9caeeb3b47d5cea7a24bd9320746059c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" proj4dart: dependency: transitive description: @@ -268,10 +260,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" unicode: dependency: transitive description: @@ -297,5 +289,5 @@ packages: source: hosted version: "2.0.0" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_services_none/pubspec.yaml b/plugins/aves_services_none/pubspec.yaml index 4d4d7651b..c189e084e 100644 --- a/plugins/aves_services_none/pubspec.yaml +++ b/plugins/aves_services_none/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.6 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: diff --git a/plugins/aves_ui/pubspec.lock b/plugins/aves_ui/pubspec.lock index c656d7419..65a37f405 100644 --- a/plugins/aves_ui/pubspec.lock +++ b/plugins/aves_ui/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" flutter: dependency: "direct main" description: flutter @@ -34,18 +34,18 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" material_color_utilities: dependency: transitive description: @@ -58,10 +58,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" sky_engine: dependency: transitive description: flutter @@ -76,4 +76,4 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0 <4.0.0" diff --git a/plugins/aves_ui/pubspec.yaml b/plugins/aves_ui/pubspec.yaml index b7efcbcc0..bc1256456 100644 --- a/plugins/aves_ui/pubspec.yaml +++ b/plugins/aves_ui/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.19.6 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: diff --git a/plugins/aves_utils/lib/aves_utils.dart b/plugins/aves_utils/lib/aves_utils.dart index 1921022e4..d41bcfd2b 100644 --- a/plugins/aves_utils/lib/aves_utils.dart +++ b/plugins/aves_utils/lib/aves_utils.dart @@ -2,3 +2,4 @@ library aves_utils; export 'src/change_notifier.dart'; export 'src/optional_event_channel.dart'; +export 'src/vector_utils.dart'; diff --git a/plugins/aves_utils/lib/src/change_notifier.dart b/plugins/aves_utils/lib/src/change_notifier.dart index 60ad5d939..1e0b0b9f5 100644 --- a/plugins/aves_utils/lib/src/change_notifier.dart +++ b/plugins/aves_utils/lib/src/change_notifier.dart @@ -1,6 +1,6 @@ import 'package:flutter/foundation.dart'; -// `ChangeNotifier` wrapper so that it can be used anywhere, not just as a mixin +// `ChangeNotifier` wrapper to call `notify` without constraint class AChangeNotifier extends ChangeNotifier { void notify() { // why is this protected? diff --git a/plugins/aves_utils/lib/src/vector_utils.dart b/plugins/aves_utils/lib/src/vector_utils.dart new file mode 100644 index 000000000..14f320784 --- /dev/null +++ b/plugins/aves_utils/lib/src/vector_utils.dart @@ -0,0 +1,15 @@ +import 'dart:ui'; + +import 'package:vector_math/vector_math_64.dart'; + +extension ExtraOffset on Offset { + Vector3 get toVector3 => Vector3(dx, dy, 0); +} + +extension ExtraVector3 on Vector3 { + Offset get toOffset => Offset(x, y); +} + +extension ExtraMatrix4 on Matrix4 { + Offset transformOffset(Offset v) => transform3(v.toVector3).toOffset; +} diff --git a/plugins/aves_utils/pubspec.lock b/plugins/aves_utils/pubspec.lock index c656d7419..f496c7561 100644 --- a/plugins/aves_utils/pubspec.lock +++ b/plugins/aves_utils/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" flutter: dependency: "direct main" description: flutter @@ -34,18 +34,18 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" material_color_utilities: dependency: transitive description: @@ -58,17 +58,17 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" vector_math: - dependency: transitive + dependency: "direct main" description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" @@ -76,4 +76,4 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0 <4.0.0" diff --git a/plugins/aves_utils/pubspec.yaml b/plugins/aves_utils/pubspec.yaml index bdd6c1210..52f2d1f3d 100644 --- a/plugins/aves_utils/pubspec.yaml +++ b/plugins/aves_utils/pubspec.yaml @@ -3,11 +3,12 @@ version: 0.0.1 publish_to: none environment: - sdk: '>=2.19.6 <3.0.0' + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter + vector_math: dev_dependencies: flutter_lints: diff --git a/plugins/aves_video/pubspec.lock b/plugins/aves_video/pubspec.lock index 35cc0e9c3..bde974666 100644 --- a/plugins/aves_video/pubspec.lock +++ b/plugins/aves_video/pubspec.lock @@ -12,18 +12,18 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" equatable: dependency: transitive description: @@ -49,18 +49,18 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" material_color_utilities: dependency: transitive description: @@ -73,10 +73,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" sky_engine: dependency: transitive description: flutter @@ -91,4 +91,4 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0 <4.0.0" diff --git a/plugins/aves_video/pubspec.yaml b/plugins/aves_video/pubspec.yaml index 7bde43cf3..7c477db6e 100644 --- a/plugins/aves_video/pubspec.yaml +++ b/plugins/aves_video/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: '>=2.19.6 <3.0.0' + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: diff --git a/pubspec.lock b/pubspec.lock index a4731aacd..14a376cdc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,50 +5,50 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "8880b4cfe7b5b17d57c052a5a3a8cc1d4f546261c7cc8fbd717bd53f48db0568" + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a url: "https://pub.dev" source: hosted - version: "59.0.0" + version: "61.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - sha256: "6a0ad72b2bcdb461749e40c01c478212a78db848dfcb2f10f2a461988bc5fb29" + sha256: "8eb354cb8ebed8a9fdf63699d15deff533bc133128898afaf754926b57d611b6" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.3.1" analyzer: dependency: transitive description: name: analyzer - sha256: a89627f49b0e70e068130a36571409726b04dab12da7e5625941d2c8ec278b96 + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 url: "https://pub.dev" source: hosted - version: "5.11.1" + version: "5.13.0" archive: dependency: transitive description: name: archive - sha256: "80e5141fafcb3361653ce308776cfd7d45e6e9fbb429e14eec571382c0c5fecb" + sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.3.7" args: dependency: transitive description: name: args - sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" async: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" aves_magnifier: dependency: "direct main" description: @@ -130,10 +130,10 @@ packages: dependency: transitive description: name: barcode - sha256: "52570564684bbb0240a9f1fdb6bad12adc5e0540103c1c96d6dd550bd928b1c9" + sha256: "789f898eef0bd88312470bdb2cc996f895ad7dd5f89e9adde84b204546a90b45" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.4" bidi: dependency: transitive description: @@ -154,25 +154,26 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" charts_common: dependency: transitive description: - name: charts_common - sha256: "7b8922f9b0d9b134122756a787dab1c3946ae4f3fc5022ff323ba0014998ea02" - url: "https://pub.dev" - source: hosted + path: charts_common + ref: aves + resolved-ref: "6c44dc05289c1a5dbf0b027dbba083669843072c" + url: "https://github.com/deckerst/flutter_google_charts.git" + source: git version: "0.12.0" charts_flutter: dependency: "direct main" description: path: charts_flutter - ref: master - resolved-ref: de76a46c1908b0c35aaf60823100fe9bfa26ae4d - url: "https://github.com/fzyzcjy/charts.git" + ref: aves + resolved-ref: "6c44dc05289c1a5dbf0b027dbba083669843072c" + url: "https://github.com/deckerst/flutter_google_charts.git" source: git version: "0.12.0" clock: @@ -187,18 +188,18 @@ packages: dependency: "direct main" description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" connectivity_plus: dependency: "direct main" description: name: connectivity_plus - sha256: b74247fad72c171381dbe700ca17da24deac637ab6d43c343b42867acb95c991 + sha256: "8599ae9edca5ff96163fca3e36f8e481ea917d1e71cdad912c084b5579913f34" url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "4.0.1" connectivity_plus_platform_interface: dependency: transitive description: @@ -235,10 +236,10 @@ packages: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" custom_rounded_rectangle_border: dependency: transitive description: @@ -267,10 +268,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903 + sha256: "499c61743e13909c13374a8c209075385858c614b9c0f2487b5f9995eeaf7369" url: "https://pub.dev" source: hosted - version: "8.2.2" + version: "9.0.1" device_info_plus_platform_interface: dependency: transitive description: @@ -283,10 +284,10 @@ packages: dependency: "direct main" description: name: dynamic_color - sha256: bbebb1b7ebed819e0ec83d4abdc2a8482d934f6a85289ffc1c6acf7589fa2aad + sha256: "74dff1435a695887ca64899b8990004f8d1232b0e84bfc4faa1fdda7c6f57cc1" url: "https://pub.dev" source: hosted - version: "1.6.3" + version: "1.6.5" equatable: dependency: "direct main" description: @@ -324,16 +325,16 @@ packages: dependency: transitive description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" fijkplayer: dependency: "direct main" description: path: "." ref: aves - resolved-ref: aebc9d7f14c8226885b6e02d7955ba6a696d454b + resolved-ref: "935a2d86ebf45fbdbaf8b4a0921d5eaea87410d6" url: "https://github.com/deckerst/fijkplayer.git" source: git version: "0.10.0" @@ -349,65 +350,66 @@ packages: dependency: transitive description: name: firebase_core - sha256: "239e4ac688674a7e7b5476fd16b0d8e2b5a453d464f32091af3ce1df4ebb7316" + sha256: "250678b816279b3240c3a33e1f76bf712c00718f1fbeffc85873a5da8c077379" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.13.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "0df0a064ab0cad7f8836291ca6f3272edd7b83ad5b3540478ee46a0849d8022b" + sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.8.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "347351a8f0518f3343d79a9a0690fa67ad232fc32e2ea270677791949eac792b" + sha256: "8c0f4c87d20e2d001a5915df238c1f9c88704231f591324205f5a5d2a7740a45" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics - sha256: "02ce958507138d938b4611e3a4910667a44b969b0c20a810bbc97bbf967ba0d7" + sha256: "0d74cca3085f144f99aa4bd82cc4d33280d4cb72bac0b733cbf97c2d7d126df8" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.3.1" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: b9c7b8498c877a2901ad323fc92c10f672be1597bc82d08f121f6228f321a7e0 + sha256: "13880033d5f2055f53bcda28024e16607b8400445a425f86732c1935da9260db" url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.6.1" flex_color_picker: dependency: "direct main" description: name: flex_color_picker - sha256: fc035dbef0975dd2650f9db1335c552e3b0ce87da4900b00f6b98cd6c78cbe42 + sha256: d8279250820ad279123fa8ee94151dd99400dd9ef4fb096589fcf956765d39a9 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: b3678d82403c13dec2ee2721e078b26f14577712411b6aa981b0f4269df3fabb + sha256: e4168a6fc88a3e5bc3d6b7a748c6a6083eedc193d343ddc26bbad7fb1b258555 url: "https://pub.dev" source: hosted - version: "1.2.4" + version: "1.3.0" floating: dependency: "direct main" description: - name: floating - sha256: c6380d9cbf628370c68749fa1f0675bc5b69d9a5b923b84660e79b3e6c9a47ec - url: "https://pub.dev" - source: hosted + path: "." + ref: main + resolved-ref: b073419d48f099b5855816a7c6e04d397b1f1c37 + url: "https://github.com/wrbl606/floating.git" + source: git version: "2.0.0" fluster: dependency: "direct main" @@ -459,6 +461,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + flutter_localization_nn: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "61b18541adb2028798cf246dda93c974120dabb8" + url: "https://github.com/deckerst/flutter_localization_nn.git" + source: git + version: "0.0.1" flutter_localizations: dependency: "direct main" description: flutter @@ -468,10 +479,10 @@ packages: dependency: "direct main" description: name: flutter_map - sha256: "59dfd14267b691bea55760786b47d3172d47cdcc0d79ff930746a5ad123491b8" + sha256: "52c65a977daae42f9aae6748418dd1535eaf27186e9bac9bf431843082bc75a3" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" flutter_markdown: dependency: "direct main" description: @@ -484,10 +495,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "8ffe990dac54a4a5492747added38571a5ab474c8e5d196809ea08849c69b1bb" + sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.15" flutter_staggered_animations: dependency: "direct main" description: @@ -523,18 +534,18 @@ packages: dependency: "direct main" description: name: get_it - sha256: f9982979e3d2f286a957c04d2c3a98f55b0f0a06ffd6c5c4abbb96f06937f463 + sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468" url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.6.0" glob: dependency: transitive description: name: glob - sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" google_api_availability: dependency: transitive description: @@ -547,10 +558,10 @@ packages: dependency: transitive description: name: google_api_availability_android - sha256: eb309bc0b435731d18f306b598e176a9afcf642089a7d7c5cbb48e393afda345 + sha256: d95429ae78083585c312de2c6578085e7d53d100a94656d691bce0bb0ce435be url: "https://pub.dev" source: hosted - version: "1.0.0+1" + version: "1.0.1" google_api_availability_platform_interface: dependency: transitive description: @@ -563,34 +574,34 @@ packages: dependency: transitive description: name: google_maps_flutter - sha256: "24392ef192f3b00bcd93151375676805a9933574423a5bd5509a0ead2e8a4215" + sha256: abefcb1e5e5c96bdd8084939dda555257af272c7972902ca46d5631092c1df68 url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.2.8" google_maps_flutter_android: dependency: transitive description: name: google_maps_flutter_android - sha256: b1aeab571b33e983ced9977f2f0eca2b25925f6319e99e3901cfc1675e1b5ada + sha256: "9512c862df77c1f0fa5f445513dd3c57f5996f0a809dccb74e54b690ee4e3a0f" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" google_maps_flutter_ios: dependency: transitive description: name: google_maps_flutter_ios - sha256: e9ad74415a222573625a2c1717adc1e375b18e8ce660fc12db734d1bda1132d4 + sha256: a9462a433bf3ebe60aadcf4906d2d6341a270d69d3e0fcaa8eb2b64699fcfb4f url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.3" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface - sha256: a07811d2b82055815ede75e1fe4b7b76f71a0b4820b26f71bdaddd157d6a3e20 + sha256: "308f0af138fa78e8224d598d46ca182673874d0ef4d754b7157c073b5b4b8e0d" url: "https://pub.dev" source: hosted - version: "2.2.6" + version: "2.2.7" highlight: dependency: transitive description: @@ -603,10 +614,10 @@ packages: dependency: transitive description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "0.13.6" http_multi_server: dependency: transitive description: @@ -627,18 +638,18 @@ packages: dependency: transitive description: name: image - sha256: "73964e3609fb96e01e69b0924b939967c556e46c7ff05db2ea9e31019000f4ef" + sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf url: "https://pub.dev" source: hosted - version: "4.0.16" + version: "4.0.17" intl: dependency: "direct main" description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.0" io: dependency: transitive description: @@ -651,26 +662,26 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" latlong2: dependency: "direct main" description: name: latlong2 - sha256: "408993a0e3f46e79ce1f129e4cb0386eef6d48dfa6394939ecacfbd7049154ec" + sha256: "08ef7282ba9f76e8495e49e2dc4d653015ac929dce5f92b375a415d30b407ea0" url: "https://pub.dev" source: hosted - version: "0.8.1" + version: "0.8.2" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" lists: dependency: transitive description: @@ -691,18 +702,18 @@ packages: dependency: transitive description: name: local_auth_android - sha256: e2b785a47b1b1dfb93147409199973c98f668ff447ccfa5543be6dd0820c537a + sha256: c5e48c4a67fc0e5dd9b5725cc8766b67e2da9a54155c82c6e2ea4a0d1cf9ef93 url: "https://pub.dev" source: hosted - version: "1.0.27" + version: "1.0.28" local_auth_ios: dependency: transitive description: name: local_auth_ios - sha256: "604078f6492fe7730fc5bb8e4f2cfe2bc287a9b499ea0ff30a29925fc1873728" + sha256: edc2977c5145492f3451db9507a2f2f284ee4f408950b3e16670838726761940 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.3" local_auth_platform_interface: dependency: transitive description: @@ -723,34 +734,34 @@ packages: dependency: transitive description: name: logging - sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" markdown: dependency: transitive description: name: markdown - sha256: d95a9d12954aafc97f984ca29baaa7690ed4d9ec4140a23ad40580bcdb6c87f5 + sha256: "8e332924094383133cee218b676871f42db2514f1f6ac617b6cf6152a7faab8e" url: "https://pub.dev" source: hosted - version: "7.0.2" + version: "7.1.0" matcher: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: "direct main" description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" material_design_icons_flutter: dependency: "direct main" description: @@ -763,10 +774,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mgrs_dart: dependency: transitive description: @@ -788,7 +799,7 @@ packages: description: path: "." ref: aves - resolved-ref: "0e02b0521beadd2be13e9a589b9a45d5cb2a06ff" + resolved-ref: "45e13ba987b00261d00488d29fa6f97ed0bcb96a" url: "https://github.com/deckerst/aves_panorama_motion_sensors.git" source: git version: "0.1.0" @@ -836,10 +847,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "10259b111176fba5c505b102e3a5b022b51dd97e30522e906d6922c745584745" + sha256: "28386bbe89ab5a7919a47cea99cdd1128e5a6e0bbd7eaafe20440ead84a15de3" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "4.0.1" package_info_plus_platform_interface: dependency: transitive description: @@ -861,7 +872,7 @@ packages: description: path: "." ref: aves - resolved-ref: "3f43082a0cc0403a906b6e6d881f370fd9caf273" + resolved-ref: dfde64310edb376a6a2536e5684de0e0a89cc31c url: "https://github.com/deckerst/aves_panorama.git" source: git version: "0.4.1" @@ -869,10 +880,10 @@ packages: dependency: "direct main" description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_parsing: dependency: transitive description: @@ -885,10 +896,10 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" + sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 url: "https://pub.dev" source: hosted - version: "2.1.10" + version: "2.1.11" path_provider_platform_interface: dependency: transitive description: @@ -917,10 +928,18 @@ packages: dependency: "direct main" description: name: pdf - sha256: "586d3debf5432e5377044754032cfa53ab45e9abf371d4865e9ad5019570e246" + sha256: "70d84154dc5b6ddf28eee6c012510a4cbbebb3a1879c0957e05364a95e8f3832" url: "https://pub.dev" source: hosted - version: "3.10.1" + version: "3.10.3" + pdf_widget_wrapper: + dependency: transitive + description: + name: pdf_widget_wrapper + sha256: e9d31fd7782ce28ae346b127ea7d1cd748d799bddee379f31191693610e23749 + url: "https://pub.dev" + source: hosted + version: "1.0.1" percent_indicator: dependency: "direct main" description: @@ -941,10 +960,10 @@ packages: dependency: transitive description: name: permission_handler_android - sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2" + sha256: d8cc6a62ded6d0f49c6eac337e080b066ee3bce4d405bd9439a61e1f1927bfe8 url: "https://pub.dev" source: hosted - version: "10.2.0" + version: "10.2.1" permission_handler_apple: dependency: transitive description: @@ -973,10 +992,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.4.0" pin_code_fields: dependency: "direct main" description: @@ -1001,6 +1020,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" polylabel: dependency: transitive description: @@ -1017,22 +1044,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - positioned_tap_detector_2: - dependency: transitive - description: - name: positioned_tap_detector_2 - sha256: "52e06863ad3e1f82b058fd05054fc8c9caeeb3b47d5cea7a24bd9320746059c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" printing: dependency: "direct main" description: name: printing - sha256: c5c19dd852e95aa140141df13fa304f079a20c4a14a66de5275a0f811240aeec + sha256: "6aa86779d51f1c60608defee7b231e1133ab9b00f63b3b71abfa85cb39898571" url: "https://pub.dev" source: hosted - version: "5.10.3" + version: "5.10.4" process: dependency: transitive description: @@ -1061,10 +1080,10 @@ packages: dependency: transitive description: name: pub_semver - sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" qr: dependency: transitive description: @@ -1085,10 +1104,10 @@ packages: dependency: transitive description: name: screen_brightness_android - sha256: "69231ea2cf83a627120302a82e98e739ee7e97c1077b58fd0ff0ad954e95a36e" + sha256: "3df10961e3a9e968a5e076fe27e7f4741fa8a1d3950bdeb48cf121ed529d0caf" url: "https://pub.dev" source: hosted - version: "0.1.0+1" + version: "0.1.0+2" screen_brightness_ios: dependency: transitive description: @@ -1125,10 +1144,10 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b" + sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_android: dependency: transitive description: @@ -1141,10 +1160,10 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07" + sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_linux: dependency: transitive description: @@ -1181,34 +1200,34 @@ packages: dependency: transitive description: name: shelf - sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler - sha256: aef74dc9195746a384843102142ab65b6a4735bb3beea791e63527b88cc83306 + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" shelf_static: dependency: transitive description: name: shelf_static - sha256: e792b76b96a36d4a41b819da593aff4bdd413576b3ba6150df5d8d9996d2e74c + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -1250,10 +1269,10 @@ packages: dependency: "direct main" description: name: sqflite - sha256: "8453780d1f703ead201a39673deb93decf85d543f359f750e2afc4908b55533f" + sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9 url: "https://pub.dev" source: hosted - version: "2.2.8" + version: "2.2.8+4" sqflite_common: dependency: transitive description: @@ -1263,7 +1282,7 @@ packages: source: hosted version: "2.4.5" stack_trace: - dependency: transitive + dependency: "direct main" description: name: stack_trace sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 @@ -1291,7 +1310,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "3c3471b548b3e8a2fbeefb9eb761f442b5cc9fbf" + resolved-ref: "096f964597703830f384d1a336e7752a6531f2a2" url: "https://github.com/deckerst/aves_streams_channel.git" source: git version: "0.3.0" @@ -1331,26 +1350,26 @@ packages: dependency: "direct dev" description: name: test - sha256: a5fcd2d25eeadbb6589e80198a47d6a464ba3e2049da473943b8af9797900c2d + sha256: "3dac9aecf2c3991d09b9cdde4f98ded7b30804a88a0d7e4e7e1678e78d6b97f4" url: "https://pub.dev" source: hosted - version: "1.22.0" + version: "1.24.1" test_api: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" test_core: dependency: transitive description: name: test_core - sha256: "0ef9755ec6d746951ba0aabe62f874b707690b5ede0fecc818b138fcc9b14888" + sha256: "5138dbffb77b2289ecb12b81c11ba46036590b72a64a7a90d6ffb880f1a29e93" url: "https://pub.dev" source: hosted - version: "0.4.20" + version: "0.5.1" transparent_image: dependency: "direct main" description: @@ -1371,10 +1390,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" unicode: dependency: transitive description: @@ -1387,18 +1406,18 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" + sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 url: "https://pub.dev" source: hosted - version: "6.1.10" + version: "6.1.11" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "22f8db4a72be26e9e3a4aa3f194b1f7afbc76d20ec141f84be1d787db2155cbd" + sha256: eed4e6a1164aa9794409325c3b707ff424d4d1c2a785e7db67f8bbda00e36e51 url: "https://pub.dev" source: hosted - version: "6.0.31" + version: "6.0.35" url_launcher_ios: dependency: transitive description: @@ -1435,10 +1454,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" + sha256: "6bb1e5d7fe53daf02a8fee85352432a40b1f868a81880e99ec7440113d5cfcab" url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.0.17" url_launcher_windows: dependency: transitive description: @@ -1448,7 +1467,7 @@ packages: source: hosted version: "3.0.6" vector_math: - dependency: transitive + dependency: "direct main" description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" @@ -1459,26 +1478,26 @@ packages: dependency: transitive description: name: vm_service - sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 + sha256: f6deed8ed625c52864792459709183da231ebf66ff0cf09e69b573227c377efe url: "https://pub.dev" source: hosted - version: "9.4.0" + version: "11.3.0" volume_controller: dependency: "direct main" description: name: volume_controller - sha256: "3bcaadd0e3298286c4ef3b758a3ea44fc98a9abe24d0e7ef087bfab72a4b6924" + sha256: "189bdc7a554f476b412e4c8b2f474562b09d74bc458c23667356bce3ca1d48c9" url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.0.7" watcher: dependency: transitive description: name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" web_socket_channel: dependency: transitive description: @@ -1491,10 +1510,10 @@ packages: dependency: transitive description: name: webdriver - sha256: ef67178f0cc7e32c1494645b11639dd1335f1d18814aa8435113a92e9ef9d841 + sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" webkit_inspection_protocol: dependency: transitive description: @@ -1507,10 +1526,18 @@ packages: dependency: transitive description: name: win32 - sha256: dd8f9344bc305ae2923e3d11a2a911d9a4e2c7dd6fe0ed10626d63211a69676e + sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "4.1.4" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae" + url: "https://pub.dev" + source: hosted + version: "1.1.0" wkt_parser: dependency: transitive description: @@ -1531,18 +1558,18 @@ packages: dependency: "direct main" description: name: xml - sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.0" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" sdks: - dart: ">=2.19.6 <3.0.0" - flutter: ">=3.7.12" + dart: ">=3.0.1 <4.0.0" + flutter: ">=3.10.2" diff --git a/pubspec.yaml b/pubspec.yaml index 1db6cfc65..d22339473 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,17 +7,14 @@ repository: https://github.com/deckerst/aves # - play changelog: /whatsnew/whatsnew-en-US # - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XX01.txt # - libre changelog: /fastlane/metadata/android/en-US/changelogs/XX.txt -version: 1.8.6+97 +version: 1.8.7+98 publish_to: none environment: # this project bundles Flutter SDK via `flutter_wrapper` # cf https://github.com/passsy/flutter_wrapper - flutter: 3.7.12 - sdk: ">=2.19.6 <3.0.0" - -# following https://github.blog/2021-09-01-improving-git-protocol-security-github/ -# dependency GitHub repos should be referenced via `https://`, not `git://` + flutter: 3.10.2 + sdk: ">=3.0.0 <4.0.0" # use `scripts/apply_flavor_{flavor}.sh` to set the right dependencies for the flavor dependencies: @@ -49,8 +46,11 @@ dependencies: path: plugins/aves_utils charts_flutter: git: - url: https://github.com/fzyzcjy/charts.git - ref: master + #url: https://github.com/fzyzcjy/charts.git + #ref: master + url: https://github.com/deckerst/flutter_google_charts.git + # fzyzcjy's master commit de76a46 (Sep 26, 2022) is incompatible with Flutter v3.10 + ref: aves path: charts_flutter collection: connectivity_plus: @@ -69,9 +69,16 @@ dependencies: ref: aves flex_color_picker: floating: + git: + url: https://github.com/wrbl606/floating.git + # v2.0.0 is incompatible with AGP8 + ref: main fluster: flutter_displaymode: flutter_highlight: + flutter_localization_nn: + git: + url: https://github.com/deckerst/flutter_localization_nn.git flutter_map: flutter_markdown: flutter_staggered_animations: @@ -98,19 +105,17 @@ dependencies: proj4dart: provider: screen_brightness: - # as of `shared_preferences` v2.0.18, upgrading packages downgrades `shared_preferences` to v0.5.4+6 - # because its dependency `shared_preferences_windows` v2.1.4 gets removed - # because its dependency `path_provider_windows` v2.1.4 gets removed - # so that the transitive `win32` gets upgraded to v4.x.x - shared_preferences: ">=2.0.0" + shared_preferences: smooth_page_indicator: sqflite: + stack_trace: streams_channel: git: url: https://github.com/deckerst/aves_streams_channel.git transparent_image: tuple: url_launcher: + vector_math: volume_controller: xml: @@ -123,6 +128,12 @@ dev_dependencies: shared_preferences_platform_interface: test: +dependency_overrides: + # as of Flutter beta v3.10.0-1.5.pre, `flutter_driver` + # constrains `material_color_utilities` to v0.2.0, which + # constrains `dynamic_color` to v1.6.4, which is incompatible with AGP8 + material_color_utilities: ^0.5.0 + flutter: assets: - assets/ @@ -162,27 +173,27 @@ flutter: # adapts from package `flutter_highlight` v0.7.0 # # `OutputBuffer` in `/services/common/output_buffer.dart` -# adapts from Flutter v3.3.3 `_OutputBuffer` in `/foundation/consolidate_response.dart` +# adapts from Flutter v3.10.0 `_OutputBuffer` in `/foundation/consolidate_response.dart` # # `TvLicensePage` in `/widgets/about/tv_license_page.dart` -# adapts from Flutter v3.7.7 `_LicenseData` in `/material/about.dart` +# adapts from Flutter v3.10.0 `_LicenseData` in `/material/about.dart` # and `_PackageLicensePage` in `/material/about.dart` # # `OverlaySnackBar` in `/widgets/common/action_mixins/overlay_snack_bar.dart` -# adapts from Flutter v3.3.3 `SnackBar` in `/material/snack_bar.dart` +# adapts from Flutter v3.10.0 `SnackBar` in `/material/snack_bar.dart` # # `EagerScaleGestureRecognizer` in `/widgets/common/behaviour/eager_scale_gesture_recognizer.dart` -# adapts from Flutter v3.3.3 `ScaleGestureRecognizer` in `/gestures/scale.dart` +# adapts from Flutter v3.10.0 `ScaleGestureRecognizer` in `/gestures/scale.dart` # # `KnownExtentScrollPhysics` in `/widgets/common/behaviour/known_extent_scroll_physics.dart` -# adapts from Flutter v3.3.3 `FixedExtentScrollPhysics` in `/widgets/list_wheel_scroll_view.dart` +# adapts from Flutter v3.10.0 `FixedExtentScrollPhysics` in `/widgets/list_wheel_scroll_view.dart` # # `TransitionImage` in `/widgets/common/fx/transition_image.dart` -# adapts from Flutter v3.3.3 `_ImageState` in `/widgets/image.dart` +# adapts from Flutter v3.10.0 `_ImageState` in `/widgets/image.dart` # and `DecorationImagePainter` in `/painting/decoration_image.dart` # # `_RenderSliverKnownExtentBoxAdaptor` in `/widgets/common/grid/sliver.dart` -# adapts from Flutter v3.3.3 `RenderSliverFixedExtentBoxAdaptor` in `/rendering/sliver_fixed_extent_list.dart` +# adapts from Flutter v3.10.0 `RenderSliverFixedExtentBoxAdaptor` in `/rendering/sliver_fixed_extent_list.dart` # # `CollectionSearchDelegate`, `SearchPageRoute` in `/widgets/search/search_delegate.dart` -# adapts from Flutter v3.3.3 `SearchDelegate`, `_SearchPageRoute` in `/material/search.dart` +# adapts from Flutter v3.10.0 `SearchDelegate`, `_SearchPageRoute` in `/material/search.dart` diff --git a/shaders.sksl.json b/shaders.sksl.json index 3d907597d..1b46c7a21 100644 --- a/shaders.sksl.json +++ b/shaders.sksl.json @@ -1 +1 @@ -{"platform":"android","name":"SM G970N","engineRevision":"1a65d409c7a1438a34d21b60bf30a6fd5db59314","data":{"DASAAAAAQAAWAABAYAAQBYH7777Z6QQBAEAAAAAAEAAAAAAAEBSAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1PVAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc1NpemVJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAADgAQAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfUzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHVDb2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCk7Cgl9CglvdXRwdXRDb2xvcl9TMCA9IG91dHB1dENvbG9yX1MwICogdGV4Q29sb3I7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAABAEAAAABJYQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAC8GAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TMV9jMF9jMF9jMC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAB5AwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAsQEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAAAAAA=","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBQU7BTXIAAAAAACAWXW3ZEQAAAADAAAAAGATHIBICYCAAAEBP2LBIPAAAAAIAAAAAEARRALJ3F5SMAAAABQAAAABABTUEURMBAAACAH5FYUHQAAAAAAAEAAAAAZ4RGGRCQFAEAAAAAAAAAGARP2LVJPAAAAAAAAEAAAABSKRXZFAUHQAAAAAAAAAACAA4AAAACAAAAAAACCAYAA":"CgAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAIIGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgMWUtMDUsIDEuMCwgMC4wLCAwLjApKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckxheW91dF9TMV9jMF9jMV9jMChfaW5wdXQpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzRfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoX3RtcF80X2luQ29sb3IpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIWJvb2woaW50KDEpKSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKF90bXBfNF9pbkNvbG9yLCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglyZXR1cm4gaGFsZjQob3V0Q29sb3IpOwp9CmhhbGY0IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEoaGFsZjQgX2lucHV0KSAKewoJX2lucHV0ID0gQ2xhbXBlZEdyYWRpZW50X1MxX2MwKF9pbnB1dCk7CgloYWxmNCBfdG1wXzVfaW5Db2xvciA9IF9pbnB1dDsKCXJldHVybiBoYWxmNChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAAAAAAA","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAACbAgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAAAAAA=","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAHSADQAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAAQAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAAAAEAAAABJYQAAAAAAAAIAAAAAWCBAAAABAAAAANAECAZAAAAAAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAPAEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzA7CnVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TMV9jMDsKdW5pZm9ybSBoYWxmMiB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFsxM107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgX2Nvb3Jkcyk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChfaW5wdXQsIGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzApICogX2Nvb3Jkcy54eTEpOwp9CmhhbGY0IFNtb290aF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBjb29yZCwgaGFsZjIgb2Zmc2V0QW5kS2VybmVsKSAKewoJcmV0dXJuIE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChjb29yZCArIG9mZnNldEFuZEtlcm5lbC54ICogdUluY3JlbWVudF9TMV9jMCkpICogb2Zmc2V0QW5kS2VybmVsLnk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBjb2xvciA9IGhhbGY0KDApOwoJZmxvYXQyIGNvb3JkID0gdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cglmb3IgKGludCBpPTA7IGk8MTM7ICsraSkgCgl7CgkJY29sb3IgKz0gU21vb3RoX1MxX2MwKF9pbnB1dCwgY29vcmQsIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwW2ldKTsKCX0KCXJldHVybiBjb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAMAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","CMRQCIAABBYAAAEIXBAAACDQMAABRAFAAAAAAAAAAAAAAAEABYAAAAEAAAAAAAEEBQAAAAA":"CgAAAExTS1MyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0MiBpbkVsbGlwc2VPZmZzZXQ7CmluIGZsb2F0NCBpbkVsbGlwc2VSYWRpaTsKb3V0IGZsb2F0MiB2RWxsaXBzZU9mZnNldHNfUzA7Cm91dCBmbG9hdDQgdkVsbGlwc2VSYWRpaV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBFbGxpcHNlR2VvbWV0cnlQcm9jZXNzb3IKCXZFbGxpcHNlT2Zmc2V0c19TMCA9IGluRWxsaXBzZU9mZnNldDsKCXZFbGxpcHNlUmFkaWlfUzAgPSBpbkVsbGlwc2VSYWRpaTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAHIDAABpbiBmbG9hdDIgdkVsbGlwc2VPZmZzZXRzX1MwOwppbiBmbG9hdDQgdkVsbGlwc2VSYWRpaV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBFbGxpcHNlR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0MiBvZmZzZXQgPSB2RWxsaXBzZU9mZnNldHNfUzAueHk7CglvZmZzZXQgKj0gdkVsbGlwc2VSYWRpaV9TMC54eTsKCWZsb2F0IHRlc3QgPSBkb3Qob2Zmc2V0LCBvZmZzZXQpIC0gMS4wOwoJZmxvYXQyIGdyYWQgPSAyLjAqb2Zmc2V0KnZFbGxpcHNlUmFkaWlfUzAueHk7CglmbG9hdCBncmFkX2RvdCA9IGRvdChncmFkLCBncmFkKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjE3NTVlLTM4KTsKCWZsb2F0IGludmxlbiA9IGludmVyc2VzcXJ0KGdyYWRfZG90KTsKCWZsb2F0IGVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNS10ZXN0Kmludmxlbik7CglvZmZzZXQgPSB2RWxsaXBzZU9mZnNldHNfUzAueHkqdkVsbGlwc2VSYWRpaV9TMC56dzsKCXRlc3QgPSBkb3Qob2Zmc2V0LCBvZmZzZXQpIC0gMS4wOwoJZ3JhZCA9IDIuMCpvZmZzZXQqdkVsbGlwc2VSYWRpaV9TMC56dzsKCWdyYWRfZG90ID0gZG90KGdyYWQsIGdyYWQpOwoJaW52bGVuID0gaW52ZXJzZXNxcnQoZ3JhZF9kb3QpOwoJZWRnZUFscGhhICo9IHNhdHVyYXRlKDAuNSt0ZXN0Kmludmxlbik7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGhhbGYoZWRnZUFscGhhKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpbkVsbGlwc2VPZmZzZXQADgAAAGluRWxsaXBzZVJhZGlpAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQAAEAQAAAAGQCBAMQAAAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAHADAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzAueSwgdWNsYW1wX1MxX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","HVJAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAABAAAAAABBAMABAAOAAAABAAAAAAABBAMAAA":"CgAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAADQAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfUzApKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAFBQATAAAAAAFAAMAAAABAAAAAAABBAMAAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAABABAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgRWxsaXB0aWNhbFJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJZmxvYXQyIFogPSBkeHkgKiB1aW52UmFkaWlYWV9TMS54eTsKCWhhbGYgaW1wbGljaXQgPSBoYWxmKGRvdChaLCBkeHkpIC0gMS4wKTsKCWhhbGYgZ3JhZF9kb3QgPSBoYWxmKDQuMCAqIGRvdChaLCBaKSk7CglncmFkX2RvdCA9IG1heChncmFkX2RvdCwgMS4wZS00KTsKCWhhbGYgYXBwcm94X2Rpc3QgPSBpbXBsaWNpdCAqIGhhbGYoaW52ZXJzZXNxcnQoZ3JhZF9kb3QpKTsKCWhhbGYgYWxwaGEgPSBjbGFtcCgwLjUgKyBhcHByb3hfZGlzdCwgMC4wLCAxLjApOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRWxsaXB0aWNhbFJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAEQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAA3AwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAB5AAAAACQHEB4XIQAQAADQAAAABAAAAAAABAEMVDOMCJKRAAAAAHAAAAAAAAAAACQAGAAAAAQAAAAAAAQQGAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAABRBQAAY29uc3QgaW50IGtGaWxsQUFfUzFfYzAgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKaGFsZjQgQ2lyY2xlX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZiBkOwoJaWYgKGludCgzKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzApIAoJewoJCWQgPSBoYWxmKChsZW5ndGgoKHVjaXJjbGVfUzFfYzAueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMV9jMC53KSAtIDEuMCkgKiB1Y2lyY2xlX1MxX2MwLnopOwoJfQoJZWxzZSAKCXsKCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1MxX2MwLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfUzFfYzAudykpICogdWNpcmNsZV9TMV9jMC56KTsKCX0KCXJldHVybiBoYWxmNChoYWxmNChpbnQoMykgPT0ga0ZpbGxBQV9TMV9jMCB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzAgPyBzYXR1cmF0ZShkKSA6IGhhbGYoZCA+IDAuNSA/IDEgOiAwKSkpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoX3NyYywgQ2lyY2xlX1MxX2MwKF9zcmMpKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IHhfcGx1c18xPXZhcmNjb29yZF9TMC54LCB5PXZhcmNjb29yZF9TMC55OwoJaGFsZiBjb3ZlcmFnZTsKCWlmICgwID09IHhfcGx1c18xKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoeSk7Cgl9CgllbHNlIAoJewoJCWZsb2F0IGZuID0geF9wbHVzXzEgKiAoeF9wbHVzXzEgLSAyKTsKCQlmbiA9IGZtYSh5LHksIGZuKTsKCQlmbG9hdCBmbndpZHRoID0gZndpZHRoKGZuKTsKCQljb3ZlcmFnZSA9IC41IC0gaGFsZihmbi9mbndpZHRoKTsKCQljb3ZlcmFnZSA9IGNsYW1wKGNvdmVyYWdlLCAwLCAxKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoY292ZXJhZ2UpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAAEQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAAA5AAAAAAABAAAAACAZAAAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgAAAABFAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2YXJjY29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAIAAEAAAABJYQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAC8GAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TMV9jMF9jMF9jMC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueik7CgljbGFtcGVkQ29vcmQueSA9IHN1YnNldENvb3JkLnk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","B2AAQAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cgl2aW5Db3ZlcmFnZV9TMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAATQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdUNvbG9yX1MwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAAAAAAA","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAADEAANAAAAALHCKLMRAAAAAAAAABAAAAAGJBCFLQVBWAQAAAAAAQAAAAAMACQCAACAAAAA2AIBAEIAAAAAAAAAAAAIADQAAAAIAAAAAAAIIDAAAAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAAeAQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBDaXJjbGVCbHVyX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TMV9jMC54eSkpICogZmxvYXQodWNpcmNsZURhdGFfUzFfYzAudykpOwoJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfUzFfYzAueikgKiB1Y2lyY2xlRGF0YV9TMV9jMC53OwoJcmV0dXJuIGhhbGY0KE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfdG1wXzBfaW5Db2xvciwgZmxvYXQyKGhhbGYyKGRpc3QsIDAuNSkpKS53d3d3KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKENpcmNsZUJsdXJfUzFfYzAoX3NyYyksIF9zcmMpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGACQAGAAAAAQAAAAAAAQQGAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAACfAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","AYTRVAADQAAAOAEARAFQJAABBADAAAILBYAACCYUQD777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7CmluIGhhbGYzIGluQ2xpcFBsYW5lOwppbiBoYWxmMyBpbklzZWN0UGxhbmU7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKb3V0IGhhbGYzIHZpbklzZWN0UGxhbmVfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5DbGlwUGxhbmVfUzAgPSBpbkNsaXBQbGFuZTsKCXZpbklzZWN0UGxhbmVfUzAgPSBpbklzZWN0UGxhbmU7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1MwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TMC55dzsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAADdAwAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKaW4gaGFsZjMgdmluSXNlY3RQbGFuZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGYzIGNsaXBQbGFuZTsKCWNsaXBQbGFuZSA9IHZpbkNsaXBQbGFuZV9TMDsKCWhhbGYzIGlzZWN0UGxhbmU7Cglpc2VjdFBsYW5lID0gdmluSXNlY3RQbGFuZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGYgY2xpcCA9IGhhbGYoc2F0dXJhdGUoY2lyY2xlRWRnZS56ICogZG90KGNpcmNsZUVkZ2UueHksIGNsaXBQbGFuZS54eSkgKyBjbGlwUGxhbmUueikpOwoJY2xpcCAqPSBoYWxmKHNhdHVyYXRlKGNpcmNsZUVkZ2UueiAqIGRvdChjaXJjbGVFZGdlLnh5LCBpc2VjdFBsYW5lLnh5KSArIGlzZWN0UGxhbmUueikpOwoJZWRnZUFscGhhICo9IGNsaXA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlCwAAAGluQ2xpcFBsYW5lAAwAAABpbklzZWN0UGxhbmUAAAAA","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGAAZAADIAAAACU53QJEKAAAAAAMAAAAAIAAAAAAGIRDFB2XASAUAABQAAAAAAAAAAAAADUAAAAAAAEAAAAAIDEAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAACrBAAAY29uc3QgaW50IGtGaWxsQUFfUzFfYzAgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpoYWxmNCBDaXJjbGVfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGQ7CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMV9jMCkgCgl7CgkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TMV9jMC54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxX2MwLncpIC0gMS4wKSAqIHVjaXJjbGVfUzFfYzAueik7Cgl9CgllbHNlIAoJewoJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfUzFfYzAueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMV9jMC53KSkgKiB1Y2lyY2xlX1MxX2MwLnopOwoJfQoJcmV0dXJuIGhhbGY0KGhhbGY0KGludCgxKSA9PSBrRmlsbEFBX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMV9jMCA/IHNhdHVyYXRlKGQpIDogaGFsZihkID4gMC41ID8gMSA6IDApKSk7Cn0KaGFsZjQgQmxlbmRfUzEoaGFsZjQgX3NyYywgaGFsZjQgX2RzdCkgCnsKCXJldHVybiBibGVuZF9tb2R1bGF0ZShfc3JjLCBDaXJjbGVfUzFfYzAoX3NyYykpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogaGFsZjQoMSkpKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb3ZlcmFnZV9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAAoAIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAuAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","EADQAAAAAEAAAAAUAABQAAQPAAABCFYMAAKAUEAAAAAAAAABAAAAAAAAAAANAAIAAAABAAAAACAJAAIAAAAA":"CgAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7CmluIGZsb2F0MyBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgZmxvYXQyIHZJbnRUZXh0dXJlQ29vcmRzX1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJdkludFRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkczsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MyBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGluUG9zaXRpb24ueHkwejsKfQoAAAAAAACfAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEaXN0YW5jZUZpZWxkUGF0aAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfUzA7CgloYWxmNCB0ZXhDb2xvcjsKCXsKCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLnJycnI7Cgl9CgloYWxmIGRpc3RhbmNlID0gNy45Njg3NSoodGV4Q29sb3IuciAtIDAuNTAxOTYwNzg0MzEpOwoJaGFsZiBhZndpZHRoOwoJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeCh2SW50VGV4dHVyZUNvb3Jkc19TMC54KSkpOwoJaGFsZiB2YWwgPSBzbW9vdGhzdGVwKC1hZndpZHRoLCBhZndpZHRoLCBkaXN0YW5jZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KHZhbCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAB3QA6AAAEAAAAAAAMAAPEAEAAABAAAAAAB2AAAAAAACAAAAAEBSAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAAeBQAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzI7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MyOwppbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglhbHBoYSA9IDEuMCAtIGFscGhhOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CmhhbGY0IENpcmN1bGFyUlJlY3RfUzIoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MyLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MyLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzIueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCWhhbGY0IG91dHB1dF9TMjsKCW91dHB1dF9TMiA9IENpcmN1bGFyUlJlY3RfUzIob3V0cHV0X1MxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMjsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA","HTQAAGAABBYAAAEIXBAAAGEAMAAAAAAAAAAAAAAAQAHAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M/AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgABAAAABQMAAGluIGZsb2F0NCB2UXVhZEVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGYgZWRnZUFscGhhOwoJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1MwLnosIHZRdWFkRWRnZV9TMC53KSArIDAuNSwgMS4wKSk7Cgl9CgllbHNlIAoJewoJCWhhbGYyIGdGID0gaGFsZjIoaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TMC54KnZRdWFkRWRnZV9TMC54IC0gdlF1YWRFZGdlX1MwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAAAAAA=","GEMAAAYAAEHAAAARC4EAAAQWBQAAAAAAAAAQAAAAIBCAAAGQAEAAAAAQAAAABAEQAEAAAAA":"CgAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAALAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAOAAAAaW5TaGFkb3dQYXJhbXMAAAAAAAA=","BYIBQAAABQAAIAABBYAAAEIXBAAP777777777777AAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1M+AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzNfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAABgEAAGluIGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAAAAAA=","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAIBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdCB2Y292ZXJhZ2VfUzA7CmZsYXQgaW4gZmxvYXQ0IHZnZW9tU3Vic2V0X1MwOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAAAAAA==","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBQU7BTXIAAAAAACAAAAAAQFV5W6JEAAAAAYAAAABQEZ2AKAWAQAABAL6SYKDYAAAACAAAAAAQEGIAAAAACAWTWL3EYAAAADAAAAACADHIJJCYCAAAEAP2LRIPAAAAAIAAAAAAABTALI3F5SOAIAABQAAAAAABTUEUZMBAAAAAH5FYUXQAAAAAAAEAAAAAZMRGOQCQFQEAAAAAAAAAGARL2LXJHAAEAAAAAEAAAABSCQX5FQUHQAAAAAAAAAACAA4AAAABAACAAAACCAYAAAAA":"CgAAAExTS1PrAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzZfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc182X1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMF9jMSkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAMMHAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMF9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc182X1MwOwpoYWxmNCBTaW5nbGVJbnRlcnZhbENvbG9yaXplcl9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8xX2Nvb3JkcyA9IF9jb29yZHM7CglyZXR1cm4gaGFsZjQobWl4KHVzdGFydF9TMV9jMF9jMF9jMCwgdWVuZF9TMV9jMF9jMF9jMCwgaGFsZihfdG1wXzFfY29vcmRzLngpKSk7Cn0KaGFsZjQgTGluZWFyTGF5b3V0X1MxX2MwX2MwX2MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMl9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfM19jb29yZHMgPSB2VHJhbnNmb3JtZWRDb29yZHNfNl9TMDsKCXJldHVybiBoYWxmNChoYWxmNChoYWxmKF90bXBfM19jb29yZHMueCkgKyAxZS0wNSwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzBfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzBfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgY29sb3JfeGZvcm1fUzFfYzAoZmxvYXQ0IGNvbG9yKSAKewoJY29sb3IucmdiICo9IGNvbG9yLmE7CglyZXR1cm4gaGFsZjQoY29sb3IpOwp9CmhhbGY0IENvbG9yU3BhY2VYZm9ybV9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gY29sb3JfeGZvcm1fUzFfYzAoQ2xhbXBlZEdyYWRpZW50X1MxX2MwX2MwKF9pbnB1dCkpOwp9CmhhbGY0IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEoaGFsZjQgX2lucHV0KSAKewoJX2lucHV0ID0gQ29sb3JTcGFjZVhmb3JtX1MxX2MwKF9pbnB1dCk7CgloYWxmNCBfdG1wXzVfaW5Db2xvciA9IF9pbnB1dDsKCXJldHVybiBoYWxmNChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gKGhhbGY0KDEuMCkgLSBvdXRwdXRfUzEpICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAAAAAA=","AYAA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1OCAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMl9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAADqAQAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFQBQU7BTXIAAAAAAAAAACAAAAAVQEAAQAAAAAQCDAEQQGAAAAAAAAAAAA4IAPAAACAAAAAAAEABYAAAAEAAAAAAAEEBQA":"CgAAAExTS1N6AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfM19TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzApICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAABAAAArAQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMjsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzI7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IE1hdHJpeEVmZmVjdF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQpoYWxmNCBDaXJjdWxhclJSZWN0X1MyKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMi5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMi5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MyLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7CgloYWxmNCBvdXRwdXRfUzI7CglvdXRwdXRfUzIgPSBDaXJjdWxhclJSZWN0X1MyKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRfUzI7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAAAAAA==","B2ABSAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1N4AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZiBpbkNvdmVyYWdlOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gdUNvbG9yX1MwOwoJY29sb3IgPSBjb2xvciAqIGluQ292ZXJhZ2U7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8zX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzFfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAGAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAAAAAA=","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAD2AAAAAAQAVSWGRIBAAADAAAAACAAAAAAQCGEIQOZLBIQAAAABQAAAAAAAAAAAAFAAMAAAABAAAAAAABBAMAAA":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAARQUAAGNvbnN0IGludCBrRmlsbEJXX1MxX2MwID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxX2MwID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxX2MwID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IFJlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGNvdmVyYWdlOwoJaWYgKGludCgxKSA9PSBrRmlsbEJXX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fUzFfYzAuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1MxX2MwLnh5LCBza19GcmFnQ29vcmQueHkpKSkpOwoJfQoJZWxzZSAKCXsKCQloYWxmNCBkaXN0czQgPSBzYXR1cmF0ZShoYWxmNCgxLjAsIDEuMCwgLTEuMCwgLTEuMCkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIHVyZWN0VW5pZm9ybV9TMV9jMCkpOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCWlmIChpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzFfYzAgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEFBX1MxX2MwKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIGhhbGY0KGhhbGY0KGNvdmVyYWdlKSk7Cn0KaGFsZjQgQmxlbmRfUzEoaGFsZjQgX3NyYywgaGFsZjQgX2RzdCkgCnsKCXJldHVybiBibGVuZF9tb2R1bGF0ZShSZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvdmVyYWdlX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAADqAQAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAAAAIAAAABLAIABAAAAABAEGABBAMAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAC2AgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TMV9jMChfc3JjKSwgX3NyYyk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb2xvcl9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAAAQAAAAGQCBAMQACAAAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAHADAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzAueCwgdWNsYW1wX1MxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAB7AgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAB5AgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKZmxhdCBpbiBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABZQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACPAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIBAIAAAABLCIIBAAAAABAEGABBAMAACAIAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADJAwAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfUzFfYzBfYzAueHksIHVjbGFtcF9TMV9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoTWF0cml4RWZmZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvbG9yX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEADZABYAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAADUAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAAYQADAAAEAFEURUKQKAAAYAAAAAAAAIAAAABSCICWKY2FAEAAAMAAAAAAAAAAAAAIADQAAAAIAAAAAAAIIDAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAB+BQAAY29uc3QgaW50IGtGaWxsQldfUzFfYzAgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzFfYzA7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgUmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzFfYzAgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxX2MwKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMV9jMC56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSk7Cgl9CgllbHNlIAoJewoJCWhhbGY0IGRpc3RzNCA9IHNhdHVyYXRlKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1MxX2MwKSk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzApIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoaGFsZjQoY292ZXJhZ2UpKTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKFJlY3RfUzFfYzAoX3NyYyksIF9zcmMpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAEAQAAAAGQCBAMQACAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAE0DAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMC54eSwgdWNsYW1wX1MxX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAKPABAAAAAAB2AAAAAAACAAAAAEBSAAAAAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAACbBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpoYWxmNCBFbGxpcHRpY2FsUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CglmbG9hdDIgWiA9IGR4eSAqIHVpbnZSYWRpaVhZX1MxLnh5OwoJaGFsZiBpbXBsaWNpdCA9IGhhbGYoZG90KFosIGR4eSkgLSAxLjApOwoJaGFsZiBncmFkX2RvdCA9IGhhbGYoNC4wICogZG90KFosIFopKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjBlLTQpOwoJaGFsZiBhcHByb3hfZGlzdCA9IGltcGxpY2l0ICogaGFsZihpbnZlcnNlc3FydChncmFkX2RvdCkpOwoJaGFsZiBhbHBoYSA9IGNsYW1wKDAuNSArIGFwcHJveF9kaXN0LCAwLjAsIDEuMCk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEVsbGlwdGljYWxSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAAAAAA==","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAGIBIAAABAAAAANAEAAAAAAAAAAAAAABAAOAAAABAAAAAAABBAMAAAAA":"CgAAAExTS1N0AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMl9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAHMCAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwKS5ycnJyOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAAAAAA=="}} \ No newline at end of file +{"platform":"android","name":"SM G970N","engineRevision":"90fa3ae28fe6ddaee1af2c120f01e50201c1401b","data":{"HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAB5AgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKZmxhdCBpbiBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAAAAAA==","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAGIBIAAABAAAAANAEAAAAAAAAAAAAAABAAOAAAABAAAAAAABBAMAAAAA":"CgAAAExTS1N0AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMl9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAHMCAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwKS5ycnJyOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAABAEAAAABJYQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAC8GAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TMV9jMF9jMF9jMC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","BYIBQAAABQAAIAABBYAAAEIXBAAP777777777777AAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1M+AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzNfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAABgEAAGluIGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAAAAAA=","CMRQCIAABBYAAAEIXBAAACDQMAABRAFAAAAAAAAAAAAAAAEABYAAAAEAAAAAAAEEBQAAAAA":"CgAAAExTS1MyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0MiBpbkVsbGlwc2VPZmZzZXQ7CmluIGZsb2F0NCBpbkVsbGlwc2VSYWRpaTsKb3V0IGZsb2F0MiB2RWxsaXBzZU9mZnNldHNfUzA7Cm91dCBmbG9hdDQgdkVsbGlwc2VSYWRpaV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBFbGxpcHNlR2VvbWV0cnlQcm9jZXNzb3IKCXZFbGxpcHNlT2Zmc2V0c19TMCA9IGluRWxsaXBzZU9mZnNldDsKCXZFbGxpcHNlUmFkaWlfUzAgPSBpbkVsbGlwc2VSYWRpaTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAHIDAABpbiBmbG9hdDIgdkVsbGlwc2VPZmZzZXRzX1MwOwppbiBmbG9hdDQgdkVsbGlwc2VSYWRpaV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBFbGxpcHNlR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0MiBvZmZzZXQgPSB2RWxsaXBzZU9mZnNldHNfUzAueHk7CglvZmZzZXQgKj0gdkVsbGlwc2VSYWRpaV9TMC54eTsKCWZsb2F0IHRlc3QgPSBkb3Qob2Zmc2V0LCBvZmZzZXQpIC0gMS4wOwoJZmxvYXQyIGdyYWQgPSAyLjAqb2Zmc2V0KnZFbGxpcHNlUmFkaWlfUzAueHk7CglmbG9hdCBncmFkX2RvdCA9IGRvdChncmFkLCBncmFkKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjE3NTVlLTM4KTsKCWZsb2F0IGludmxlbiA9IGludmVyc2VzcXJ0KGdyYWRfZG90KTsKCWZsb2F0IGVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNS10ZXN0Kmludmxlbik7CglvZmZzZXQgPSB2RWxsaXBzZU9mZnNldHNfUzAueHkqdkVsbGlwc2VSYWRpaV9TMC56dzsKCXRlc3QgPSBkb3Qob2Zmc2V0LCBvZmZzZXQpIC0gMS4wOwoJZ3JhZCA9IDIuMCpvZmZzZXQqdkVsbGlwc2VSYWRpaV9TMC56dzsKCWdyYWRfZG90ID0gZG90KGdyYWQsIGdyYWQpOwoJaW52bGVuID0gaW52ZXJzZXNxcnQoZ3JhZF9kb3QpOwoJZWRnZUFscGhhICo9IHNhdHVyYXRlKDAuNSt0ZXN0Kmludmxlbik7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGhhbGYoZWRnZUFscGhhKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpbkVsbGlwc2VPZmZzZXQADgAAAGluRWxsaXBzZVJhZGlpAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAB5AwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA","B2ABSAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1N4AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZiBpbkNvdmVyYWdlOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gdUNvbG9yX1MwOwoJY29sb3IgPSBjb2xvciAqIGluQ292ZXJhZ2U7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8zX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzFfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAGAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAAYQADAAAEAFEURUKQKAAAYAAAAAAAAIAAAABSCICWKY2FAEAAAMAAAAAAAAAAAAAIADQAAAAIAAAAAAAIIDAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAB+BQAAY29uc3QgaW50IGtGaWxsQldfUzFfYzAgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzFfYzA7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgUmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzFfYzAgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxX2MwKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMV9jMC56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSk7Cgl9CgllbHNlIAoJewoJCWhhbGY0IGRpc3RzNCA9IHNhdHVyYXRlKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1MxX2MwKSk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzApIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoaGFsZjQoY292ZXJhZ2UpKTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKFJlY3RfUzFfYzAoX3NyYyksIF9zcmMpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAA7AwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9IChibGVuZF9tb2R1bGF0ZShzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHRleENvb3JkKSwgaGFsZjQoMSkpKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAAAQAAAAGQCBAMQACAAAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAHADAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzAueCwgdWNsYW1wX1MxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAAEQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAAA5AAAAAAABAAAAACAZAAAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgAAAABFAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2YXJjY29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFQBQU7BTXIAAAAAAAAAACAAAAAVQEAAQAAAAAQCDAEQQGAAAAAAAAAAAA4IAPAAACAAAAAAAEABYAAAAEAAAAAAAEEBQA":"CgAAAExTS1N6AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfM19TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzApICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAABAAAArAQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMjsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzI7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IE1hdHJpeEVmZmVjdF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQpoYWxmNCBDaXJjdWxhclJSZWN0X1MyKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMi5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMi5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MyLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7CgloYWxmNCBvdXRwdXRfUzI7CglvdXRwdXRfUzIgPSBDaXJjdWxhclJSZWN0X1MyKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRfUzI7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIBAIAAAABLCIIBAAAAABAEGABBAMAACAIAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADJAwAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfUzFfYzBfYzAueHksIHVjbGFtcF9TMV9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoTWF0cml4RWZmZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvbG9yX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","AYTRVAADQAAAOAEARAFQJAABBADAAAILBYAACCYUQD777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7CmluIGhhbGYzIGluQ2xpcFBsYW5lOwppbiBoYWxmMyBpbklzZWN0UGxhbmU7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKb3V0IGhhbGYzIHZpbklzZWN0UGxhbmVfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5DbGlwUGxhbmVfUzAgPSBpbkNsaXBQbGFuZTsKCXZpbklzZWN0UGxhbmVfUzAgPSBpbklzZWN0UGxhbmU7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1MwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TMC55dzsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAADdAwAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKaW4gaGFsZjMgdmluSXNlY3RQbGFuZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGYzIGNsaXBQbGFuZTsKCWNsaXBQbGFuZSA9IHZpbkNsaXBQbGFuZV9TMDsKCWhhbGYzIGlzZWN0UGxhbmU7Cglpc2VjdFBsYW5lID0gdmluSXNlY3RQbGFuZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGYgY2xpcCA9IGhhbGYoc2F0dXJhdGUoY2lyY2xlRWRnZS56ICogZG90KGNpcmNsZUVkZ2UueHksIGNsaXBQbGFuZS54eSkgKyBjbGlwUGxhbmUueikpOwoJY2xpcCAqPSBoYWxmKHNhdHVyYXRlKGNpcmNsZUVkZ2UueiAqIGRvdChjaXJjbGVFZGdlLnh5LCBpc2VjdFBsYW5lLnh5KSArIGlzZWN0UGxhbmUueikpOwoJZWRnZUFscGhhICo9IGNsaXA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlCwAAAGluQ2xpcFBsYW5lAAwAAABpbklzZWN0UGxhbmUAAAAA","HVJAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAABAAAAAABBAMABAAOAAAABAAAAAAABBAMAAA":"CgAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAADdAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoYmxlbmRfbW9kdWxhdGUoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCksIG91dHB1dENvbG9yX1MwKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAAAAAA==","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBQU7BTXIAAAAAACAAAAAAQFV5W6JEAAAAAYAAAABQEZ2AKAWAQAABAL6SYKDYAAAACAAAAAAQEGIAAAAACAWTWL3EYAAAADAAAAACADHIJJCYCAAAEAP2LRIPAAAAAIAAAAAAABTALI3F5SOAIAABQAAAAAABTUEUZMBAAAAAH5FYUXQAAAAAAAEAAAAAZMRGOQCQFQEAAAAAAAAAGARL2LXJHAAEAAAAAEAAAABSCQX5FQUHQAAAAAAAAAACAA4AAAABAACAAAACCAYAAAAA":"CgAAAExTS1PrAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzZfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc182X1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMF9jMSkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAMMHAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMF9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc182X1MwOwpoYWxmNCBTaW5nbGVJbnRlcnZhbENvbG9yaXplcl9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8xX2Nvb3JkcyA9IF9jb29yZHM7CglyZXR1cm4gaGFsZjQobWl4KHVzdGFydF9TMV9jMF9jMF9jMCwgdWVuZF9TMV9jMF9jMF9jMCwgaGFsZihfdG1wXzFfY29vcmRzLngpKSk7Cn0KaGFsZjQgTGluZWFyTGF5b3V0X1MxX2MwX2MwX2MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMl9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfM19jb29yZHMgPSB2VHJhbnNmb3JtZWRDb29yZHNfNl9TMDsKCXJldHVybiBoYWxmNChoYWxmNChoYWxmKF90bXBfM19jb29yZHMueCkgKyAxZS0wNSwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzBfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzBfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgY29sb3JfeGZvcm1fUzFfYzAoZmxvYXQ0IGNvbG9yKSAKewoJY29sb3IucmdiICo9IGNvbG9yLmE7CglyZXR1cm4gaGFsZjQoY29sb3IpOwp9CmhhbGY0IENvbG9yU3BhY2VYZm9ybV9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gY29sb3JfeGZvcm1fUzFfYzAoQ2xhbXBlZEdyYWRpZW50X1MxX2MwX2MwKF9pbnB1dCkpOwp9CmhhbGY0IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEoaGFsZjQgX2lucHV0KSAKewoJX2lucHV0ID0gQ29sb3JTcGFjZVhmb3JtX1MxX2MwKF9pbnB1dCk7CgloYWxmNCBfdG1wXzVfaW5Db2xvciA9IF9pbnB1dDsKCXJldHVybiBoYWxmNChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gKGhhbGY0KDEuMCkgLSBvdXRwdXRfUzEpICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEADZABYAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAADUAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAHSADQAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAAQAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAAAAAA=","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAAoAIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","B2AAQAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cgl2aW5Db3ZlcmFnZV9TMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAATQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdUNvbG9yX1MwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAADqAQAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAMAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAEAQAAAAGQCBAMQACAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAE0DAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMC54eSwgdWNsYW1wX1MxX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAAAAAA=","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAACbAgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAAAAAA=","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAIBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdCB2Y292ZXJhZ2VfUzA7CmZsYXQgaW4gZmxvYXQ0IHZnZW9tU3Vic2V0X1MwOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAAAAAA==","AYAA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1OCAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMl9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAADqAQAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQAAEAQAAAAGQCBAMQAAAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAHADAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzAueSwgdWNsYW1wX1MxX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAsQEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAAAAAA=","HTQAAGAABBYAAAEIXBAAAGEAMAAAAAAAAAAAAAAAQAHAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M/AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgABAAAABQMAAGluIGZsb2F0NCB2UXVhZEVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGYgZWRnZUFscGhhOwoJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1MwLnosIHZRdWFkRWRnZV9TMC53KSArIDAuNSwgMS4wKSk7Cgl9CgllbHNlIAoJewoJCWhhbGYyIGdGID0gaGFsZjIoaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TMC54KnZRdWFkRWRnZV9TMC54IC0gdlF1YWRFZGdlX1MwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAAAAAA=","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBQU7BTXIAAAAAACAWXW3ZEQAAAADAAAAAGATHIBICYCAAAEBP2LBIPAAAAAIAAAAAEARRALJ3F5SMAAAABQAAAABABTUEURMBAAACAH5FYUHQAAAAAAAEAAAAAZ4RGGRCQFAEAAAAAAAAAGARP2LVJPAAAAAAAAEAAAABSKRXZFAUHQAAAAAAAAAACAA4AAAACAAAAAAACCAYAA":"CgAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAIIGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgMWUtMDUsIDEuMCwgMC4wLCAwLjApKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckxheW91dF9TMV9jMF9jMV9jMChfaW5wdXQpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzRfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoX3RtcF80X2luQ29sb3IpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIWJvb2woaW50KDEpKSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKF90bXBfNF9pbkNvbG9yLCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglyZXR1cm4gaGFsZjQob3V0Q29sb3IpOwp9CmhhbGY0IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEoaGFsZjQgX2lucHV0KSAKewoJX2lucHV0ID0gQ2xhbXBlZEdyYWRpZW50X1MxX2MwKF9pbnB1dCk7CgloYWxmNCBfdG1wXzVfaW5Db2xvciA9IF9pbnB1dDsKCXJldHVybiBoYWxmNChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAAAAAAA","EADQAAAAAEAAAAAUAABQAAQPAAABCFYMAAKAUEAAAAAAAAABAAAAAAAAAAANAAIAAAABAAAAACAJAAIAAAAA":"CgAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7CmluIGZsb2F0MyBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgZmxvYXQyIHZJbnRUZXh0dXJlQ29vcmRzX1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJdkludFRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkczsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MyBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGluUG9zaXRpb24ueHkwejsKfQoAAAAAAACfAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEaXN0YW5jZUZpZWxkUGF0aAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfUzA7CgloYWxmNCB0ZXhDb2xvcjsKCXsKCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLnJycnI7Cgl9CgloYWxmIGRpc3RhbmNlID0gNy45Njg3NSoodGV4Q29sb3IuciAtIDAuNTAxOTYwNzg0MzEpOwoJaGFsZiBhZndpZHRoOwoJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeCh2SW50VGV4dHVyZUNvb3Jkc19TMC54KSkpOwoJaGFsZiB2YWwgPSBzbW9vdGhzdGVwKC1hZndpZHRoLCBhZndpZHRoLCBkaXN0YW5jZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KHZhbCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAD2AAAAAAQAVSWGRIBAAADAAAAACAAAAAAQCGEIQOZLBIQAAAABQAAAAAAAAAAAAFAAMAAAABAAAAAAABBAMAAA":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAARQUAAGNvbnN0IGludCBrRmlsbEJXX1MxX2MwID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxX2MwID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxX2MwID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IFJlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGNvdmVyYWdlOwoJaWYgKGludCgxKSA9PSBrRmlsbEJXX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fUzFfYzAuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1MxX2MwLnh5LCBza19GcmFnQ29vcmQueHkpKSkpOwoJfQoJZWxzZSAKCXsKCQloYWxmNCBkaXN0czQgPSBzYXR1cmF0ZShoYWxmNCgxLjAsIDEuMCwgLTEuMCwgLTEuMCkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIHVyZWN0VW5pZm9ybV9TMV9jMCkpOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCWlmIChpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzFfYzAgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEFBX1MxX2MwKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIGhhbGY0KGhhbGY0KGNvdmVyYWdlKSk7Cn0KaGFsZjQgQmxlbmRfUzEoaGFsZjQgX3NyYywgaGFsZjQgX2RzdCkgCnsKCXJldHVybiBibGVuZF9tb2R1bGF0ZShSZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvdmVyYWdlX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAB3QA6AAAEAAAAAAAMAAPEAEAAABAAAAAAB2AAAAAAACAAAAAEBSAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAAeBQAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzI7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MyOwppbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglhbHBoYSA9IDEuMCAtIGFscGhhOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CmhhbGY0IENpcmN1bGFyUlJlY3RfUzIoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MyLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MyLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzIueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCWhhbGY0IG91dHB1dF9TMjsKCW91dHB1dF9TMiA9IENpcmN1bGFyUlJlY3RfUzIob3V0cHV0X1MxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMjsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAAAAIAAAABLAIABAAAAABAEGABBAMAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAC2AgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TMV9jMChfc3JjKSwgX3NyYyk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb2xvcl9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGAAZAADIAAAACU53QJEKAAAAAAMAAAAAIAAAAAAGIRDFB2XASAUAABQAAAAAAAAAAAAADUAAAAAAAEAAAAAIDEAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAC4BAAAY29uc3QgaW50IGtGaWxsQUFfUzFfYzAgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpoYWxmNCBDaXJjbGVfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGQ7CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMV9jMCkgCgl7CgkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TMV9jMC54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxX2MwLncpIC0gMS4wKSAqIHVjaXJjbGVfUzFfYzAueik7Cgl9CgllbHNlIAoJewoJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfUzFfYzAueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMV9jMC53KSkgKiB1Y2lyY2xlX1MxX2MwLnopOwoJfQoJcmV0dXJuIGhhbGY0KGhhbGY0KGludCgxKSA9PSBrRmlsbEFBX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMV9jMCA/IHNhdHVyYXRlKGQpIDogaGFsZihkID4gMC41ID8gMSA6IDApKSk7Cn0KaGFsZjQgQmxlbmRfUzEoaGFsZjQgX3NyYywgaGFsZjQgX2RzdCkgCnsKCXJldHVybiBibGVuZF9tb2R1bGF0ZShfc3JjLCBDaXJjbGVfUzFfYzAoX3NyYykpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoYmxlbmRfbW9kdWxhdGUoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCksIGhhbGY0KDEpKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","GEMAAAYAAEHAAAARC4EAAAQWBQAAAAAAAAAQAAAAIBCAAAGQAEAAAAAQAAAABAEQAEAAAAA":"CgAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAALAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAOAAAAaW5TaGFkb3dQYXJhbXMAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABZQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACPAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAIAAEAAAABJYQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAC8GAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TMV9jMF9jMF9jMC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueik7CgljbGFtcGVkQ29vcmQueSA9IHN1YnNldENvb3JkLnk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGACQAGAAAAAQAAAAAAAQQGAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAACsAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9IChibGVuZF9tb2R1bGF0ZShzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHRleENvb3JkKSwgaGFsZjQoMSkpKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAAAAEAAAABJYQAAAAAAAAIAAAAAWCBAAAABAAAAANAECAZAAAAAAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAPAEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzA7CnVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TMV9jMDsKdW5pZm9ybSBoYWxmMiB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFsxM107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgX2Nvb3Jkcyk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChfaW5wdXQsIGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzApICogX2Nvb3Jkcy54eTEpOwp9CmhhbGY0IFNtb290aF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBjb29yZCwgaGFsZjIgb2Zmc2V0QW5kS2VybmVsKSAKewoJcmV0dXJuIE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChjb29yZCArIG9mZnNldEFuZEtlcm5lbC54ICogdUluY3JlbWVudF9TMV9jMCkpICogb2Zmc2V0QW5kS2VybmVsLnk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBjb2xvciA9IGhhbGY0KDApOwoJZmxvYXQyIGNvb3JkID0gdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cglmb3IgKGludCBpPTA7IGk8MTM7ICsraSkgCgl7CgkJY29sb3IgKz0gU21vb3RoX1MxX2MwKF9pbnB1dCwgY29vcmQsIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwW2ldKTsKCX0KCXJldHVybiBjb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAB7AgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAKPABAAAAAAB2AAAAAAACAAAAAEBSAAAAAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAACbBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpoYWxmNCBFbGxpcHRpY2FsUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CglmbG9hdDIgWiA9IGR4eSAqIHVpbnZSYWRpaVhZX1MxLnh5OwoJaGFsZiBpbXBsaWNpdCA9IGhhbGYoZG90KFosIGR4eSkgLSAxLjApOwoJaGFsZiBncmFkX2RvdCA9IGhhbGYoNC4wICogZG90KFosIFopKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjBlLTQpOwoJaGFsZiBhcHByb3hfZGlzdCA9IGltcGxpY2l0ICogaGFsZihpbnZlcnNlc3FydChncmFkX2RvdCkpOwoJaGFsZiBhbHBoYSA9IGNsYW1wKDAuNSArIGFwcHJveF9kaXN0LCAwLjAsIDEuMCk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEVsbGlwdGljYWxSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAB5AAAAACQHEB4XIQAQAADQAAAABAAAAAAABAEMVDOMCJKRAAAAAHAAAAAAAAAAACQAGAAAAAQAAAAAAAQQGAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAABRBQAAY29uc3QgaW50IGtGaWxsQUFfUzFfYzAgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKaGFsZjQgQ2lyY2xlX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZiBkOwoJaWYgKGludCgzKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzApIAoJewoJCWQgPSBoYWxmKChsZW5ndGgoKHVjaXJjbGVfUzFfYzAueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMV9jMC53KSAtIDEuMCkgKiB1Y2lyY2xlX1MxX2MwLnopOwoJfQoJZWxzZSAKCXsKCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1MxX2MwLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfUzFfYzAudykpICogdWNpcmNsZV9TMV9jMC56KTsKCX0KCXJldHVybiBoYWxmNChoYWxmNChpbnQoMykgPT0ga0ZpbGxBQV9TMV9jMCB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzAgPyBzYXR1cmF0ZShkKSA6IGhhbGYoZCA+IDAuNSA/IDEgOiAwKSkpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoX3NyYywgQ2lyY2xlX1MxX2MwKF9zcmMpKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IHhfcGx1c18xPXZhcmNjb29yZF9TMC54LCB5PXZhcmNjb29yZF9TMC55OwoJaGFsZiBjb3ZlcmFnZTsKCWlmICgwID09IHhfcGx1c18xKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoeSk7Cgl9CgllbHNlIAoJewoJCWZsb2F0IGZuID0geF9wbHVzXzEgKiAoeF9wbHVzXzEgLSAyKTsKCQlmbiA9IGZtYSh5LHksIGZuKTsKCQlmbG9hdCBmbndpZHRoID0gZndpZHRoKGZuKTsKCQljb3ZlcmFnZSA9IC41IC0gaGFsZihmbi9mbndpZHRoKTsKCQljb3ZlcmFnZSA9IGNsYW1wKGNvdmVyYWdlLCAwLCAxKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoY292ZXJhZ2UpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","DASAAAAAQAAWAABAYAAQBYH7777Z6QQBAEAAAAAAEAAAAAAAEBSAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1PVAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc1NpemVJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAADgAQAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfUzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHVDb2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCk7Cgl9CglvdXRwdXRDb2xvcl9TMCA9IG91dHB1dENvbG9yX1MwICogdGV4Q29sb3I7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAADEAANAAAAALHCKLMRAAAAAAAAABAAAAAGJBCFLQVBWAQAAAAAAQAAAAAMACQCAACAAAAA2AIBAEIAAAAAAAAAAAAIADQAAAAIAAAAAAAIIDAAAAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAAeAQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBDaXJjbGVCbHVyX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TMV9jMC54eSkpICogZmxvYXQodWNpcmNsZURhdGFfUzFfYzAudykpOwoJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfUzFfYzAueikgKiB1Y2lyY2xlRGF0YV9TMV9jMC53OwoJcmV0dXJuIGhhbGY0KE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfdG1wXzBfaW5Db2xvciwgZmxvYXQyKGhhbGYyKGRpc3QsIDAuNSkpKS53d3d3KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKENpcmNsZUJsdXJfUzFfYzAoX3NyYyksIF9zcmMpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAAAAAA="}} \ No newline at end of file diff --git a/test/model/view_state_test.dart b/test/model/view_state_test.dart new file mode 100644 index 000000000..7da0dcb84 --- /dev/null +++ b/test/model/view_state_test.dart @@ -0,0 +1,90 @@ +import 'dart:ui'; + +import 'package:aves/model/view_state.dart'; +import 'package:aves_utils/aves_utils.dart'; +import 'package:test/test.dart'; +import 'package:vector_math/vector_math_64.dart'; + +void main() { + test('scene -> viewport, original scaleFit', () { + const viewport = Rect.fromLTWH(0, 0, 100, 200); + const content = Rect.fromLTWH(0, 0, 200, 400); + final state = ViewState(position: Offset.zero, scale: 1, viewportSize: viewport.size, contentSize: content.size); + + expect(_toViewportPoint(state, content.topLeft), const Offset(-50, -100)); + expect(_toViewportPoint(state, content.bottomRight), const Offset(150, 300)); + }); + + test('scene -> viewport, scaled to fit .5', () { + const viewport = Rect.fromLTWH(0, 0, 100, 200); + const content = Rect.fromLTWH(0, 0, 200, 400); + final state = ViewState(position: Offset.zero, scale: .5, viewportSize: viewport.size, contentSize: content.size); + + expect(_toViewportPoint(state, content.topLeft), viewport.topLeft); + expect(_toViewportPoint(state, content.center), viewport.center); + expect(_toViewportPoint(state, content.bottomRight), viewport.bottomRight); + }); + + test('scene -> viewport, scaled to fit .25', () { + const viewport = Rect.fromLTWH(0, 0, 50, 100); + const content = Rect.fromLTWH(0, 0, 200, 400); + final state = ViewState(position: Offset.zero, scale: .25, viewportSize: viewport.size, contentSize: content.size); + + expect(_toViewportPoint(state, content.topLeft), viewport.topLeft); + expect(_toViewportPoint(state, content.center), viewport.center); + expect(_toViewportPoint(state, content.bottomRight), viewport.bottomRight); + }); + + test('viewport -> scene, original scaleFit', () { + const viewport = Rect.fromLTWH(0, 0, 100, 200); + const content = Rect.fromLTWH(0, 0, 200, 400); + final state = ViewState(position: Offset.zero, scale: 1, viewportSize: viewport.size, contentSize: content.size); + + expect(_toContentPoint(state, viewport.topLeft), const Offset(50, 100)); + expect(_toContentPoint(state, viewport.bottomRight), const Offset(150, 300)); + }); + + test('viewport -> scene, scaled to fit', () { + const viewport = Rect.fromLTWH(0, 0, 100, 200); + const content = Rect.fromLTWH(0, 0, 200, 400); + final state = ViewState(position: Offset.zero, scale: .5, viewportSize: viewport.size, contentSize: content.size); + + expect(_toContentPoint(state, viewport.topLeft), content.topLeft); + expect(_toContentPoint(state, viewport.center), content.center); + expect(_toContentPoint(state, viewport.bottomRight), content.bottomRight); + }); + + test('viewport -> scene, translated', () { + const viewport = Rect.fromLTWH(0, 0, 100, 200); + const content = Rect.fromLTWH(0, 0, 200, 400); + final state = ViewState(position: const Offset(50, 50), scale: 1, viewportSize: viewport.size, contentSize: content.size); + + _toContentPoint(state, viewport.topLeft); + expect(_toContentPoint(state, viewport.topLeft), const Offset(0, 50)); + expect(_toContentPoint(state, viewport.bottomRight), const Offset(100, 250)); + }); + + test('scene -> viewport, scaled to fit, different ratios', () { + const viewport = Rect.fromLTWH(0, 0, 360, 521); + const content = Rect.fromLTWH(0, 0, 2268, 4032); + final scaleFit = viewport.height / content.height; + final state = ViewState(position: Offset.zero, scale: scaleFit, viewportSize: viewport.size, contentSize: content.size); + + final scaledContentLeft = (viewport.width - content.width * scaleFit) / 2; + final scaledContentRight = viewport.width - scaledContentLeft; + + expect(_toViewportPoint(state, content.topLeft), Offset(scaledContentLeft, 0)); + expect(_toViewportPoint(state, content.center), viewport.center); + expect(_toViewportPoint(state, content.bottomRight), Offset(scaledContentRight, viewport.bottom)); + }); +} + +// convenience methods + +Offset _toViewportPoint(ViewState state, Offset contentPoint) { + return state.matrix.transformOffset(contentPoint); +} + +Offset _toContentPoint(ViewState state, viewportPoint) { + return Matrix4.inverted(state.matrix).transformOffset(viewportPoint); +} diff --git a/test/utils/math_utils_test.dart b/test/utils/math_utils_test.dart index bb8f5cae2..8032d07c9 100644 --- a/test/utils/math_utils_test.dart +++ b/test/utils/math_utils_test.dart @@ -1,5 +1,8 @@ +import 'dart:ui'; + import 'package:aves/utils/math_utils.dart'; import 'package:test/test.dart'; +import 'package:tuple/tuple.dart'; void main() { test('highest power of 2 that is smaller than or equal to the number', () { @@ -24,4 +27,10 @@ void main() { expect(roundToPrecision(1.2345678, decimals: 3), 1.235); expect(roundToPrecision(0, decimals: 3), 0); }); + + test('segment intersection', () { + const s1 = Tuple2(Offset(1, 1), Offset(3, 2)); + const s2 = Tuple2(Offset(1, 4), Offset(2, -1)); + expect(segmentIntersection(s1, s2), const Offset(17 / 11, 14 / 11)); + }); } diff --git a/untranslated.json b/untranslated.json index 1f27c1cb1..e70c1f140 100644 --- a/untranslated.json +++ b/untranslated.json @@ -6,6 +6,8 @@ "timeMinutes", "timeDays", "focalLength", + "saveCopyButtonLabel", + "applyTooltip", "pickTooltip", "sourceStateLoading", "sourceStateCataloguing", @@ -75,6 +77,12 @@ "entryInfoActionRemoveMetadata", "entryInfoActionExportMetadata", "entryInfoActionRemoveLocation", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", "filterBinLabel", @@ -170,6 +178,7 @@ "widgetOpenPageHome", "widgetOpenPageCollection", "widgetOpenPageViewer", + "widgetTapUpdateWidget", "storageVolumeDescriptionFallbackPrimary", "storageVolumeDescriptionFallbackNonPrimary", "rootDirectoryDescription", @@ -378,7 +387,6 @@ "albumCamera", "albumDownload", "albumScreenshots", - "albumScreenRecordings", "albumVideoCaptures", "albumPageTitle", "albumEmpty", @@ -625,6 +633,8 @@ ], "ckb": [ + "saveCopyButtonLabel", + "applyTooltip", "chipActionGoToPlacePage", "chipActionShowCountryStates", "entryActionRotateCCW", @@ -654,6 +664,12 @@ "entryInfoActionRemoveMetadata", "entryInfoActionExportMetadata", "entryInfoActionRemoveLocation", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", "filterBinLabel", @@ -749,6 +765,7 @@ "widgetOpenPageHome", "widgetOpenPageCollection", "widgetOpenPageViewer", + "widgetTapUpdateWidget", "storageVolumeDescriptionFallbackPrimary", "storageVolumeDescriptionFallbackNonPrimary", "rootDirectoryDescription", @@ -1217,48 +1234,14 @@ ], "cs": [ - "maxBrightnessNever", - "maxBrightnessAlways", - "videoResumptionModeNever", - "videoResumptionModeAlways", - "exportEntryDialogQuality", - "settingsAskEverytime", - "settingsVideoPlaybackTile", - "settingsVideoPlaybackPageTitle", - "settingsVideoResumptionModeTile", - "settingsVideoResumptionModeDialogTitle", - "tagEditorDiscardDialogMessage" - ], - - "de": [ - "maxBrightnessNever", - "maxBrightnessAlways", - "videoResumptionModeNever", - "videoResumptionModeAlways", - "exportEntryDialogQuality", - "settingsAskEverytime", - "settingsVideoPlaybackTile", - "settingsVideoPlaybackPageTitle", - "settingsVideoResumptionModeTile", - "settingsVideoResumptionModeDialogTitle", - "tagEditorDiscardDialogMessage" - ], - - "eu": [ - "maxBrightnessNever", - "maxBrightnessAlways", - "videoResumptionModeNever", - "videoResumptionModeAlways", - "exportEntryDialogQuality", - "settingsAskEverytime", - "settingsVideoPlaybackTile", - "settingsVideoPlaybackPageTitle", - "settingsVideoResumptionModeTile", - "settingsVideoResumptionModeDialogTitle", - "tagEditorDiscardDialogMessage" + "editorActionTransform", + "cropAspectRatioFree", + "cropAspectRatioSquare" ], "fa": [ + "saveCopyButtonLabel", + "applyTooltip", "clearTooltip", "chipActionGoToPlacePage", "chipActionLock", @@ -1271,6 +1254,12 @@ "viewerActionLock", "viewerActionUnlock", "slideshowActionResume", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", "filterLocatedLabel", @@ -1323,6 +1312,7 @@ "widgetOpenPageHome", "widgetOpenPageCollection", "widgetOpenPageViewer", + "widgetTapUpdateWidget", "rootDirectoryDescription", "otherDirectoryDescription", "restrictedAccessDialogMessage", @@ -1755,6 +1745,8 @@ "gl": [ "columnCount", + "saveCopyButtonLabel", + "applyTooltip", "chipActionGoToPlacePage", "chipActionLock", "chipActionShowCountryStates", @@ -1766,6 +1758,12 @@ "viewerActionUnlock", "entryInfoActionExportMetadata", "entryInfoActionRemoveLocation", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", "filterNoAddressLabel", @@ -1813,6 +1811,7 @@ "widgetOpenPageHome", "widgetOpenPageCollection", "widgetOpenPageViewer", + "widgetTapUpdateWidget", "storageVolumeDescriptionFallbackPrimary", "storageVolumeDescriptionFallbackNonPrimary", "rootDirectoryDescription", @@ -2293,6 +2292,8 @@ "showButtonLabel", "hideButtonLabel", "continueButtonLabel", + "saveCopyButtonLabel", + "applyTooltip", "cancelTooltip", "changeTooltip", "clearTooltip", @@ -2374,6 +2375,12 @@ "entryInfoActionRemoveMetadata", "entryInfoActionExportMetadata", "entryInfoActionRemoveLocation", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", "filterBinLabel", @@ -2469,6 +2476,7 @@ "widgetOpenPageHome", "widgetOpenPageCollection", "widgetOpenPageViewer", + "widgetTapUpdateWidget", "storageVolumeDescriptionFallbackPrimary", "storageVolumeDescriptionFallbackNonPrimary", "rootDirectoryDescription", @@ -2937,6 +2945,8 @@ ], "hi": [ + "saveCopyButtonLabel", + "applyTooltip", "resetTooltip", "saveTooltip", "pickTooltip", @@ -3010,6 +3020,12 @@ "entryInfoActionRemoveMetadata", "entryInfoActionExportMetadata", "entryInfoActionRemoveLocation", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", "filterBinLabel", @@ -3105,6 +3121,7 @@ "widgetOpenPageHome", "widgetOpenPageCollection", "widgetOpenPageViewer", + "widgetTapUpdateWidget", "storageVolumeDescriptionFallbackPrimary", "storageVolumeDescriptionFallbackNonPrimary", "rootDirectoryDescription", @@ -3572,22 +3589,33 @@ "filePickerUseThisFolder" ], - "id": [ - "exportEntryDialogQuality", - "settingsVideoPlaybackTile", - "settingsVideoPlaybackPageTitle", - "settingsVideoResumptionModeTile", - "settingsVideoResumptionModeDialogTitle", - "tagEditorDiscardDialogMessage" + "it": [ + "saveCopyButtonLabel", + "applyTooltip", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", + "widgetTapUpdateWidget" ], "ja": [ "columnCount", + "saveCopyButtonLabel", + "applyTooltip", "chipActionShowCountryStates", "chipActionCreateVault", "chipActionConfigureVault", "viewerActionLock", "viewerActionUnlock", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "filterLocatedLabel", "filterTaggedLabel", "albumTierVaults", @@ -3596,6 +3624,7 @@ "subtitlePositionBottom", "videoResumptionModeNever", "videoResumptionModeAlways", + "widgetTapUpdateWidget", "vaultBinUsageDialogMessage", "exportEntryDialogQuality", "exportEntryDialogWriteMetadata", @@ -3625,6 +3654,8 @@ "lt": [ "columnCount", + "saveCopyButtonLabel", + "applyTooltip", "chipActionGoToPlacePage", "chipActionLock", "chipActionShowCountryStates", @@ -3632,6 +3663,12 @@ "chipActionConfigureVault", "viewerActionLock", "viewerActionUnlock", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "filterLocatedLabel", "filterTaggedLabel", "albumTierVaults", @@ -3646,6 +3683,7 @@ "settingsVideoEnablePip", "videoResumptionModeNever", "videoResumptionModeAlways", + "widgetTapUpdateWidget", "newVaultWarningDialogMessage", "newVaultDialogTitle", "configureVaultDialogTitle", @@ -3690,133 +3728,237 @@ "tagPlaceholderState" ], - "nb": [ - "chipActionShowCountryStates", - "viewerActionLock", - "viewerActionUnlock", - "maxBrightnessNever", - "maxBrightnessAlways", - "vaultLockTypePattern", - "settingsVideoEnablePip", - "videoResumptionModeNever", - "videoResumptionModeAlways", - "patternDialogEnter", - "patternDialogConfirm", - "exportEntryDialogQuality", - "statePageTitle", - "stateEmpty", - "searchStatesSectionTitle", - "settingsAskEverytime", - "settingsCollectionBurstPatternsTile", - "settingsCollectionBurstPatternsNone", - "settingsVideoPlaybackTile", - "settingsVideoPlaybackPageTitle", - "settingsVideoResumptionModeTile", - "settingsVideoResumptionModeDialogTitle", - "settingsVideoBackgroundMode", - "settingsVideoBackgroundModeDialogTitle", - "statsTopStatesSectionTitle", - "tagEditorDiscardDialogMessage", - "tagPlaceholderState" - ], - - "nl": [ - "columnCount", - "chipActionGoToPlacePage", - "chipActionLock", - "chipActionShowCountryStates", - "entryActionShareImageOnly", - "entryActionShareVideoOnly", - "viewerActionLock", - "viewerActionUnlock", - "filterLocatedLabel", - "albumTierVaults", - "maxBrightnessNever", - "maxBrightnessAlways", - "subtitlePositionTop", - "subtitlePositionBottom", - "vaultLockTypePattern", - "videoResumptionModeNever", - "videoResumptionModeAlways", - "newVaultWarningDialogMessage", - "newVaultDialogTitle", - "configureVaultDialogTitle", - "vaultDialogLockModeWhenScreenOff", - "vaultDialogLockTypeLabel", - "patternDialogEnter", - "patternDialogConfirm", - "pinDialogEnter", - "pinDialogConfirm", - "passwordDialogEnter", - "passwordDialogConfirm", - "authenticateToConfigureVault", - "authenticateToUnlockVault", - "vaultBinUsageDialogMessage", - "exportEntryDialogQuality", - "exportEntryDialogWriteMetadata", - "tooManyItemsErrorDialogMessage", - "drawerPlacePage", - "statePageTitle", - "stateEmpty", - "placePageTitle", - "placeEmpty", - "searchStatesSectionTitle", - "settingsAskEverytime", - "settingsModificationWarningDialogMessage", - "settingsConfirmationVaultDataLoss", - "settingsCollectionBurstPatternsTile", - "settingsCollectionBurstPatternsNone", - "settingsViewerShowRatingTags", - "settingsViewerShowDescription", - "settingsVideoPlaybackTile", - "settingsVideoPlaybackPageTitle", - "settingsVideoResumptionModeTile", - "settingsVideoResumptionModeDialogTitle", - "settingsVideoBackgroundMode", - "settingsVideoBackgroundModeDialogTitle", - "settingsVideoGestureVerticalDragBrightnessVolume", - "settingsSubtitleThemeTextPositionTile", - "settingsSubtitleThemeTextPositionDialogTitle", - "settingsDisablingBinWarningDialogMessage", - "settingsAccessibilityShowPinchGestureAlternatives", - "settingsDisplayUseTvInterface", - "settingsWidgetDisplayedItem", - "statsTopStatesSectionTitle", - "tagEditorDiscardDialogMessage", - "tagPlaceholderState" - ], - - "nn": [ + "ml": [ + "appName", + "welcomeMessage", + "welcomeOptional", + "welcomeTermsToggle", + "itemCount", "columnCount", + "timeSeconds", + "timeMinutes", + "timeDays", + "focalLength", + "applyButtonLabel", + "deleteButtonLabel", + "nextButtonLabel", + "showButtonLabel", + "hideButtonLabel", + "continueButtonLabel", + "saveCopyButtonLabel", + "applyTooltip", + "cancelTooltip", + "changeTooltip", + "clearTooltip", + "previousTooltip", + "nextTooltip", + "showTooltip", + "hideTooltip", + "actionRemove", + "resetTooltip", + "saveTooltip", + "pickTooltip", + "doubleBackExitMessage", + "doNotAskAgain", + "sourceStateLoading", "sourceStateCataloguing", + "sourceStateLocatingCountries", + "sourceStateLocatingPlaces", + "chipActionDelete", + "chipActionGoToAlbumPage", + "chipActionGoToCountryPage", "chipActionGoToPlacePage", + "chipActionGoToTagPage", + "chipActionFilterOut", + "chipActionFilterIn", + "chipActionHide", "chipActionLock", + "chipActionPin", + "chipActionUnpin", + "chipActionRename", + "chipActionSetCover", "chipActionShowCountryStates", + "chipActionCreateAlbum", "chipActionCreateVault", "chipActionConfigureVault", + "entryActionCopyToClipboard", + "entryActionDelete", + "entryActionConvert", + "entryActionExport", + "entryActionInfo", + "entryActionRename", + "entryActionRestore", + "entryActionRotateCCW", + "entryActionRotateCW", + "entryActionFlip", + "entryActionPrint", + "entryActionShare", + "entryActionShareImageOnly", + "entryActionShareVideoOnly", + "entryActionViewSource", + "entryActionShowGeoTiffOnMap", + "entryActionConvertMotionPhotoToStillImage", + "entryActionViewMotionPhotoVideo", + "entryActionEdit", + "entryActionOpen", + "entryActionSetAs", + "entryActionOpenMap", + "entryActionRotateScreen", + "entryActionAddFavourite", + "entryActionRemoveFavourite", + "videoActionCaptureFrame", + "videoActionMute", + "videoActionUnmute", + "videoActionPause", + "videoActionPlay", + "videoActionReplay10", + "videoActionSkip10", + "videoActionSelectStreams", + "videoActionSetSpeed", + "viewerActionSettings", "viewerActionLock", "viewerActionUnlock", + "slideshowActionResume", + "slideshowActionShowInCollection", + "entryInfoActionEditDate", + "entryInfoActionEditLocation", + "entryInfoActionEditTitleDescription", + "entryInfoActionEditRating", + "entryInfoActionEditTags", + "entryInfoActionRemoveMetadata", + "entryInfoActionExportMetadata", "entryInfoActionRemoveLocation", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", + "filterAspectRatioLandscapeLabel", + "filterAspectRatioPortraitLabel", + "filterBinLabel", + "filterFavouriteLabel", + "filterNoDateLabel", + "filterNoAddressLabel", "filterLocatedLabel", "filterNoLocationLabel", + "filterNoRatingLabel", "filterTaggedLabel", + "filterNoTagLabel", + "filterNoTitleLabel", + "filterOnThisDayLabel", + "filterRecentlyAddedLabel", + "filterRatingRejectedLabel", + "filterTypeAnimatedLabel", + "filterTypeMotionPhotoLabel", + "filterTypePanoramaLabel", + "filterTypeRawLabel", + "filterTypeSphericalVideoLabel", + "filterTypeGeotiffLabel", + "filterMimeImageLabel", + "filterMimeVideoLabel", + "accessibilityAnimationsRemove", "accessibilityAnimationsKeep", + "albumTierNew", + "albumTierPinned", + "albumTierSpecial", + "albumTierApps", "albumTierVaults", + "albumTierRegular", + "coordinateFormatDms", + "coordinateFormatDecimal", + "coordinateDms", + "coordinateDmsNorth", + "coordinateDmsSouth", + "coordinateDmsEast", + "coordinateDmsWest", "displayRefreshRatePreferHighest", "displayRefreshRatePreferLowest", + "keepScreenOnNever", + "keepScreenOnVideoPlayback", + "keepScreenOnViewerOnly", + "keepScreenOnAlways", "lengthUnitPixel", "lengthUnitPercent", + "mapStyleGoogleNormal", + "mapStyleGoogleHybrid", + "mapStyleGoogleTerrain", + "mapStyleHuaweiNormal", + "mapStyleHuaweiTerrain", + "mapStyleOsmHot", + "mapStyleStamenToner", + "mapStyleStamenWatercolor", "maxBrightnessNever", "maxBrightnessAlways", + "nameConflictStrategyRename", + "nameConflictStrategyReplace", + "nameConflictStrategySkip", + "subtitlePositionTop", + "subtitlePositionBottom", + "themeBrightnessLight", + "themeBrightnessDark", + "themeBrightnessBlack", + "unitSystemMetric", + "unitSystemImperial", "vaultLockTypePattern", "vaultLockTypePin", "vaultLockTypePassword", "settingsVideoEnablePip", + "videoControlsPlay", + "videoControlsPlaySeek", + "videoControlsPlayOutside", + "videoControlsNone", + "videoLoopModeNever", + "videoLoopModeShortOnly", + "videoLoopModeAlways", + "videoPlaybackSkip", + "videoPlaybackMuted", + "videoPlaybackWithSound", "videoResumptionModeNever", "videoResumptionModeAlways", + "viewerTransitionSlide", + "viewerTransitionParallax", + "viewerTransitionFade", + "viewerTransitionZoomIn", + "viewerTransitionNone", "wallpaperTargetHome", + "wallpaperTargetLock", "wallpaperTargetHomeLock", + "widgetDisplayedItemRandom", + "widgetDisplayedItemMostRecent", + "widgetOpenPageHome", + "widgetOpenPageCollection", + "widgetOpenPageViewer", + "widgetTapUpdateWidget", + "storageVolumeDescriptionFallbackPrimary", + "storageVolumeDescriptionFallbackNonPrimary", + "rootDirectoryDescription", + "otherDirectoryDescription", + "storageAccessDialogMessage", + "restrictedAccessDialogMessage", + "notEnoughSpaceDialogMessage", + "missingSystemFilePickerDialogMessage", + "unsupportedTypeDialogMessage", + "nameConflictDialogSingleSourceMessage", + "nameConflictDialogMultipleSourceMessage", + "addShortcutDialogLabel", + "addShortcutButtonLabel", + "noMatchingAppDialogMessage", + "binEntriesConfirmationDialogMessage", + "deleteEntriesConfirmationDialogMessage", + "moveUndatedConfirmationDialogMessage", + "moveUndatedConfirmationDialogSetDate", + "videoResumeDialogMessage", + "videoStartOverButtonLabel", + "videoResumeButtonLabel", + "setCoverDialogLatest", + "setCoverDialogAuto", "setCoverDialogCustom", + "hideFilterConfirmationDialogMessage", + "newAlbumDialogTitle", + "newAlbumDialogNameLabel", + "newAlbumDialogNameLabelAlreadyExistsHelper", + "newAlbumDialogStorageLabel", "newVaultWarningDialogMessage", "newVaultDialogTitle", "configureVaultDialogTitle", @@ -3831,22 +3973,86 @@ "authenticateToConfigureVault", "authenticateToUnlockVault", "vaultBinUsageDialogMessage", + "renameAlbumDialogLabel", + "renameAlbumDialogLabelAlreadyExistsHelper", + "renameEntrySetPageTitle", + "renameEntrySetPagePatternFieldLabel", + "renameEntrySetPageInsertTooltip", + "renameEntrySetPagePreviewSectionTitle", + "renameProcessorCounter", + "renameProcessorName", + "deleteSingleAlbumConfirmationDialogMessage", + "deleteMultiAlbumConfirmationDialogMessage", + "exportEntryDialogFormat", + "exportEntryDialogWidth", + "exportEntryDialogHeight", "exportEntryDialogQuality", "exportEntryDialogWriteMetadata", + "renameEntryDialogLabel", + "editEntryDialogCopyFromItem", "editEntryDialogTargetFieldsHeader", + "editEntryDateDialogTitle", "editEntryDateDialogSetCustom", + "editEntryDateDialogCopyField", + "editEntryDateDialogExtractFromTitle", + "editEntryDateDialogShift", + "editEntryDateDialogSourceFileModifiedDate", + "durationDialogHours", + "durationDialogMinutes", + "durationDialogSeconds", "editEntryLocationDialogTitle", "editEntryLocationDialogSetCustom", + "editEntryLocationDialogChooseOnMap", + "editEntryLocationDialogLatitude", + "editEntryLocationDialogLongitude", "locationPickerUseThisLocationButton", + "editEntryRatingDialogTitle", + "removeEntryMetadataDialogTitle", + "removeEntryMetadataDialogMore", "removeEntryMetadataMotionPhotoXmpWarningDialogMessage", + "videoSpeedDialogLabel", + "videoStreamSelectionDialogVideo", + "videoStreamSelectionDialogAudio", + "videoStreamSelectionDialogText", + "videoStreamSelectionDialogOff", + "videoStreamSelectionDialogTrack", + "videoStreamSelectionDialogNoSelection", + "genericSuccessFeedback", + "genericFailureFeedback", + "genericDangerWarningDialogMessage", "tooManyItemsErrorDialogMessage", + "menuActionConfigureView", + "menuActionSelect", + "menuActionSelectAll", + "menuActionSelectNone", + "menuActionMap", + "menuActionSlideshow", + "menuActionStats", "viewDialogSortSectionTitle", + "viewDialogGroupSectionTitle", + "viewDialogLayoutSectionTitle", "viewDialogReverseSortOrder", + "tileLayoutMosaic", + "tileLayoutGrid", + "tileLayoutList", + "coverDialogTabCover", + "coverDialogTabApp", + "coverDialogTabColor", + "appPickDialogTitle", + "appPickDialogNone", + "aboutPageTitle", + "aboutLinkLicense", "aboutLinkPolicy", + "aboutBugSectionTitle", "aboutBugSaveLogInstruction", + "aboutBugCopyInfoInstruction", + "aboutBugCopyInfoButton", "aboutBugReportInstruction", "aboutBugReportButton", "aboutCreditsSectionTitle", + "aboutCreditsWorldAtlas1", + "aboutCreditsWorldAtlas2", + "aboutTranslatorsSectionTitle", "aboutLicensesSectionTitle", "aboutLicensesBanner", "aboutLicensesAndroidLibrariesSectionTitle", @@ -4106,27 +4312,231 @@ "settingsRemoveAnimationsDialogTitle", "settingsTimeToTakeActionTile", "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplaySectionTitle", "settingsThemeBrightnessTile", "settingsThemeBrightnessDialogTitle", + "settingsThemeColorHighlights", "settingsThemeEnableDynamicColor", "settingsDisplayRefreshRateModeTile", "settingsDisplayRefreshRateModeDialogTitle", "settingsDisplayUseTvInterface", + "settingsLanguageSectionTitle", + "settingsLanguageTile", + "settingsLanguagePageTitle", + "settingsCoordinateFormatTile", + "settingsCoordinateFormatDialogTitle", + "settingsUnitSystemTile", + "settingsUnitSystemDialogTitle", + "settingsScreenSaverPageTitle", + "settingsWidgetPageTitle", + "settingsWidgetShowOutline", "settingsWidgetOpenPage", + "settingsWidgetDisplayedItem", + "settingsCollectionTile", + "statsPageTitle", "statsWithGps", + "statsTopCountriesSectionTitle", "statsTopStatesSectionTitle", + "statsTopPlacesSectionTitle", + "statsTopTagsSectionTitle", + "statsTopAlbumsSectionTitle", + "viewerOpenPanoramaButtonLabel", + "viewerSetWallpaperButtonLabel", + "viewerErrorUnknown", + "viewerErrorDoesNotExist", "viewerInfoPageTitle", + "viewerInfoBackToViewerTooltip", + "viewerInfoUnknown", "viewerInfoLabelDescription", + "viewerInfoLabelTitle", + "viewerInfoLabelDate", + "viewerInfoLabelResolution", + "viewerInfoLabelSize", + "viewerInfoLabelUri", + "viewerInfoLabelPath", + "viewerInfoLabelDuration", + "viewerInfoLabelOwner", + "viewerInfoLabelCoordinates", + "viewerInfoLabelAddress", + "mapStyleDialogTitle", + "mapStyleTooltip", + "mapZoomInTooltip", + "mapZoomOutTooltip", "mapPointNorthUpTooltip", "mapAttributionOsmHot", "mapAttributionStamen", + "openMapPageTooltip", "mapEmptyRegion", + "viewerInfoOpenEmbeddedFailureFeedback", + "viewerInfoOpenLinkText", + "viewerInfoViewXmlLinkText", + "viewerInfoSearchFieldLabel", + "viewerInfoSearchEmpty", + "viewerInfoSearchSuggestionDate", + "viewerInfoSearchSuggestionDescription", "viewerInfoSearchSuggestionDimensions", + "viewerInfoSearchSuggestionResolution", + "viewerInfoSearchSuggestionRights", "wallpaperUseScrollEffect", + "tagEditorPageTitle", + "tagEditorPageNewTagFieldLabel", + "tagEditorPageAddTagTooltip", + "tagEditorSectionRecent", + "tagEditorSectionPlaceholders", + "tagEditorDiscardDialogMessage", + "tagPlaceholderCountry", + "tagPlaceholderState", + "tagPlaceholderPlace", + "panoramaEnableSensorControl", + "panoramaDisableSensorControl", + "sourceViewerPageTitle", + "filePickerShowHiddenFiles", + "filePickerDoNotShowHiddenFiles", + "filePickerOpenFrom", + "filePickerNoItems", + "filePickerUseThisFolder" + ], + + "nb": [ + "saveCopyButtonLabel", + "applyTooltip", + "chipActionShowCountryStates", + "viewerActionLock", + "viewerActionUnlock", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", + "maxBrightnessNever", + "maxBrightnessAlways", + "vaultLockTypePattern", + "settingsVideoEnablePip", + "videoResumptionModeNever", + "videoResumptionModeAlways", + "widgetTapUpdateWidget", + "patternDialogEnter", + "patternDialogConfirm", + "exportEntryDialogQuality", + "statePageTitle", + "stateEmpty", + "searchStatesSectionTitle", + "settingsAskEverytime", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", + "settingsVideoPlaybackTile", + "settingsVideoPlaybackPageTitle", + "settingsVideoResumptionModeTile", + "settingsVideoResumptionModeDialogTitle", + "settingsVideoBackgroundMode", + "settingsVideoBackgroundModeDialogTitle", + "statsTopStatesSectionTitle", "tagEditorDiscardDialogMessage", "tagPlaceholderState" ], + "nl": [ + "columnCount", + "saveCopyButtonLabel", + "applyTooltip", + "chipActionGoToPlacePage", + "chipActionLock", + "chipActionShowCountryStates", + "entryActionShareImageOnly", + "entryActionShareVideoOnly", + "viewerActionLock", + "viewerActionUnlock", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", + "filterLocatedLabel", + "albumTierVaults", + "maxBrightnessNever", + "maxBrightnessAlways", + "subtitlePositionTop", + "subtitlePositionBottom", + "vaultLockTypePattern", + "videoResumptionModeNever", + "videoResumptionModeAlways", + "widgetTapUpdateWidget", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "patternDialogEnter", + "patternDialogConfirm", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", + "exportEntryDialogQuality", + "exportEntryDialogWriteMetadata", + "tooManyItemsErrorDialogMessage", + "drawerPlacePage", + "statePageTitle", + "stateEmpty", + "placePageTitle", + "placeEmpty", + "searchStatesSectionTitle", + "settingsAskEverytime", + "settingsModificationWarningDialogMessage", + "settingsConfirmationVaultDataLoss", + "settingsCollectionBurstPatternsTile", + "settingsCollectionBurstPatternsNone", + "settingsViewerShowRatingTags", + "settingsViewerShowDescription", + "settingsVideoPlaybackTile", + "settingsVideoPlaybackPageTitle", + "settingsVideoResumptionModeTile", + "settingsVideoResumptionModeDialogTitle", + "settingsVideoBackgroundMode", + "settingsVideoBackgroundModeDialogTitle", + "settingsVideoGestureVerticalDragBrightnessVolume", + "settingsSubtitleThemeTextPositionTile", + "settingsSubtitleThemeTextPositionDialogTitle", + "settingsDisablingBinWarningDialogMessage", + "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface", + "settingsWidgetDisplayedItem", + "statsTopStatesSectionTitle", + "tagEditorDiscardDialogMessage", + "tagPlaceholderState" + ], + + "nn": [ + "sourceStateCataloguing", + "accessibilityAnimationsKeep", + "settingsVideoEnablePip", + "widgetTapUpdateWidget", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "viewDialogSortSectionTitle", + "viewDialogReverseSortOrder", + "aboutCreditsSectionTitle", + "aboutLicensesBanner", + "aboutLicensesAndroidLibrariesSectionTitle", + "collectionActionShowTitleSearch", + "collectionActionHideTitleSearch", + "drawerCollectionAnimated", + "settingsSlideshowAnimatedZoomEffect", + "settingsHiddenItemsTabFilters", + "settingsHiddenFiltersBanner", + "settingsHiddenFiltersEmpty", + "settingsRemoveAnimationsTile", + "settingsRemoveAnimationsDialogTitle", + "settingsWidgetOpenPage", + "viewerInfoSearchSuggestionDimensions", + "wallpaperUseScrollEffect" + ], + "or": [ "appName", "welcomeMessage", @@ -4141,6 +4551,8 @@ "deleteButtonLabel", "nextButtonLabel", "continueButtonLabel", + "saveCopyButtonLabel", + "applyTooltip", "cancelTooltip", "changeTooltip", "clearTooltip", @@ -4216,6 +4628,12 @@ "entryInfoActionRemoveMetadata", "entryInfoActionExportMetadata", "entryInfoActionRemoveLocation", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", "filterBinLabel", @@ -4300,6 +4718,7 @@ "widgetOpenPageHome", "widgetOpenPageCollection", "widgetOpenPageViewer", + "widgetTapUpdateWidget", "storageVolumeDescriptionFallbackPrimary", "storageVolumeDescriptionFallbackNonPrimary", "rootDirectoryDescription", @@ -4718,24 +5137,31 @@ ], "pt": [ - "maxBrightnessNever", - "maxBrightnessAlways", - "videoResumptionModeNever", - "videoResumptionModeAlways", - "exportEntryDialogQuality", - "settingsAskEverytime", - "settingsVideoPlaybackTile", - "settingsVideoPlaybackPageTitle", - "settingsVideoResumptionModeTile", - "settingsVideoResumptionModeDialogTitle", - "tagEditorDiscardDialogMessage" + "saveCopyButtonLabel", + "applyTooltip", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", + "widgetTapUpdateWidget" ], "ro": [ + "saveCopyButtonLabel", + "applyTooltip", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "maxBrightnessNever", "maxBrightnessAlways", "videoResumptionModeNever", "videoResumptionModeAlways", + "widgetTapUpdateWidget", "exportEntryDialogQuality", "settingsAskEverytime", "settingsVideoPlaybackTile", @@ -4746,10 +5172,19 @@ ], "ru": [ + "saveCopyButtonLabel", + "applyTooltip", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "maxBrightnessNever", "maxBrightnessAlways", "videoResumptionModeNever", "videoResumptionModeAlways", + "widgetTapUpdateWidget", "exportEntryDialogQuality", "statePageTitle", "stateEmpty", @@ -4770,6 +5205,8 @@ "itemCount", "columnCount", "timeSeconds", + "saveCopyButtonLabel", + "applyTooltip", "chipActionGoToPlacePage", "chipActionLock", "chipActionShowCountryStates", @@ -4777,6 +5214,12 @@ "chipActionConfigureVault", "viewerActionLock", "viewerActionUnlock", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "filterLocatedLabel", "filterNoLocationLabel", "albumTierVaults", @@ -4791,6 +5234,7 @@ "settingsVideoEnablePip", "videoResumptionModeNever", "videoResumptionModeAlways", + "widgetTapUpdateWidget", "otherDirectoryDescription", "storageAccessDialogMessage", "restrictedAccessDialogMessage", @@ -5218,6 +5662,8 @@ "timeDays", "focalLength", "applyButtonLabel", + "saveCopyButtonLabel", + "applyTooltip", "chipActionGoToPlacePage", "chipActionLock", "chipActionShowCountryStates", @@ -5225,6 +5671,12 @@ "chipActionConfigureVault", "viewerActionLock", "viewerActionUnlock", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "albumTierVaults", "lengthUnitPixel", "lengthUnitPercent", @@ -5236,6 +5688,7 @@ "settingsVideoEnablePip", "videoResumptionModeNever", "videoResumptionModeAlways", + "widgetTapUpdateWidget", "newVaultWarningDialogMessage", "newVaultDialogTitle", "configureVaultDialogTitle", @@ -5590,6 +6043,8 @@ ], "tr": [ + "saveCopyButtonLabel", + "applyTooltip", "chipActionGoToPlacePage", "chipActionLock", "chipActionShowCountryStates", @@ -5597,6 +6052,12 @@ "chipActionConfigureVault", "viewerActionLock", "viewerActionUnlock", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "albumTierVaults", "lengthUnitPixel", "lengthUnitPercent", @@ -5608,6 +6069,7 @@ "settingsVideoEnablePip", "videoResumptionModeNever", "videoResumptionModeAlways", + "widgetTapUpdateWidget", "newVaultWarningDialogMessage", "newVaultDialogTitle", "configureVaultDialogTitle", @@ -5646,24 +6108,18 @@ "tagPlaceholderState" ], - "uk": [ - "maxBrightnessNever", - "maxBrightnessAlways", - "videoResumptionModeNever", - "videoResumptionModeAlways", - "exportEntryDialogQuality", - "settingsAskEverytime", - "settingsVideoPlaybackTile", - "settingsVideoPlaybackPageTitle", - "settingsVideoResumptionModeTile", - "settingsVideoResumptionModeDialogTitle", - "tagEditorDiscardDialogMessage" - ], - "zh": [ + "saveCopyButtonLabel", + "applyTooltip", "chipActionGoToPlacePage", "viewerActionLock", "viewerActionUnlock", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "filterLocatedLabel", "filterTaggedLabel", "albumTierVaults", @@ -5677,6 +6133,7 @@ "settingsVideoEnablePip", "videoResumptionModeNever", "videoResumptionModeAlways", + "widgetTapUpdateWidget", "newVaultWarningDialogMessage", "newVaultDialogTitle", "configureVaultDialogTitle", @@ -5723,6 +6180,8 @@ "zh_Hant": [ "columnCount", + "saveCopyButtonLabel", + "applyTooltip", "chipActionGoToPlacePage", "chipActionLock", "chipActionShowCountryStates", @@ -5730,6 +6189,12 @@ "chipActionConfigureVault", "viewerActionLock", "viewerActionUnlock", + "editorActionTransform", + "editorTransformCrop", + "editorTransformRotate", + "cropAspectRatioFree", + "cropAspectRatioOriginal", + "cropAspectRatioSquare", "filterLocatedLabel", "filterTaggedLabel", "albumTierVaults", @@ -5743,6 +6208,7 @@ "settingsVideoEnablePip", "videoResumptionModeNever", "videoResumptionModeAlways", + "widgetTapUpdateWidget", "newVaultWarningDialogMessage", "newVaultDialogTitle", "configureVaultDialogTitle", diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index f0c1457ff..58bdc5c44 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,5 +1,5 @@ -In v1.8.6: -- more settings for video resumption and conversion quality -- support for Sony predictive capture bursts -- enjoy the app in Hungarian +In v1.8.7: +- play your animated PNGs +- set your home to the Tags page +- enjoy the app in Norwegian (Nynorsk) Full changelog available on GitHub \ No newline at end of file