diff --git a/.flutter b/.flutter index 135454af3..994429713 160000 --- a/.flutter +++ b/.flutter @@ -1 +1 @@ -Subproject commit 135454af32477f815a7525073027a3ff9eff1bfd +Subproject commit 9944297138845a94256f1cf37beb88ff9a8e811a diff --git a/CHANGELOG.md b/CHANGELOG.md index 896ce5ddd..b148456f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [v1.8.0] - 2023-02-20 + +### Added + +- Vaults +- Viewer: overlay details expand/collapse on tap +- Viewer: export actions available as quick actions +- Slideshow: added settings quick action +- TV: improved support for Info +- Basque translation (thanks Aitor Salaberria) + +### Changed + +- disabling the recycle bin will delete forever items in it +- remember pin status of albums becoming empty +- allow setting dates before 1970/01/01 +- upgraded Flutter to stable v3.7.3 + +### Fixed + +- SD card access grant on Android Lollipop +- copying to SD card in some cases +- sharing SD card files referred by `file` URI + ## [v1.7.10] - 2023-01-18 ### Added diff --git a/android/app/build.gradle b/android/app/build.gradle index 051231057..b974a5ee6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -18,6 +18,9 @@ if (localPropertiesFile.exists()) { def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionName = localProperties.getProperty('flutter.versionName') def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" // Keys @@ -181,10 +184,11 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' implementation 'androidx.core:core-ktx:1.9.0' - implementation 'androidx.exifinterface:exifinterface:1.3.5' + implementation 'androidx.exifinterface:exifinterface:1.3.6' implementation 'androidx.lifecycle:lifecycle-process:2.5.1' implementation 'androidx.media:media:1.6.0' implementation 'androidx.multidex:multidex:2.0.1' + implementation 'androidx.security:security-crypto:1.1.0-alpha04' implementation 'com.caverock:androidsvg-aar:1.4' implementation 'com.commonsware.cwac:document:0.5.0' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5d8bc1ce9..7553cb54f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ This change eventually prevents building the app with Flutter v3.3.3. android:required="false" /> @@ -32,28 +32,35 @@ This change eventually prevents building the app with Flutter v3.3.3. android:maxSdkVersion="29" tools:ignore="ScopedStorage" /> + + + + + + - - - - - - - - + + + + - - + - - + + @@ -75,12 +82,14 @@ This change eventually prevents building the app with Flutter v3.3.3. android:allowBackup="true" android:appCategory="image" android:banner="@drawable/banner" + android:dataExtractionRules="@xml/data_extraction_rules" + android:fullBackupContent="@xml/full_backup_content" android:fullBackupOnly="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:requestLegacyExternalStorage="true" android:roundIcon="@mipmap/ic_launcher_round" - tools:targetApi="o"> + tools:targetApi="s"> when (call.method) { "configure" -> { @@ -42,9 +47,9 @@ class HomeWidgetSettingsActivity : MainActivity() { } private fun saveWidget() { - val appWidgetManager = AppWidgetManager.getInstance(context) + val appWidgetManager = AppWidgetManager.getInstance(this) val widgetInfo = appWidgetManager.getAppWidgetOptions(appWidgetId) - HomeWidgetProvider().onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, widgetInfo) + HomeWidgetProvider().onAppWidgetOptionsChanged(this, appWidgetManager, appWidgetId, widgetInfo) val intent = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) setResult(RESULT_OK, intent) 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 307eadc55..20961ea40 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt @@ -90,7 +90,7 @@ class HomeWidgetProvider : AppWidgetProvider() { val messenger = flutterEngine!!.dartExecutor val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL) try { - val bytes = suspendCoroutine { cont -> + val bytes = suspendCoroutine { cont -> defaultScope.launch { FlutterUtils.runOnUiThread { channel.invokeMethod("drawWidget", hashMapOf( @@ -194,7 +194,10 @@ class HomeWidgetProvider : AppWidgetProvider() { } private fun initChannels(context: Context) { - val messenger = flutterEngine!!.dartExecutor + val engine = flutterEngine + engine ?: throw Exception("Flutter engine is not initialized") + + val messenger = engine.dartExecutor // dart -> platform -> dart // - need Context 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 d4b3ee148..cfd60ec3e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -25,14 +25,15 @@ import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering import deckers.thibault.aves.utils.FlutterUtils.isSoftwareRenderingRequired import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.getParcelableExtraCompat -import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.android.FlutterFragmentActivity +import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap -open class MainActivity : FlutterActivity() { +open class MainActivity : FlutterFragmentActivity() { private lateinit var mediaStoreChangeStreamHandler: MediaStoreChangeStreamHandler private lateinit var settingsChangeStreamHandler: SettingsChangeStreamHandler private lateinit var intentStreamHandler: IntentStreamHandler @@ -68,8 +69,12 @@ open class MainActivity : FlutterActivity() { // .build() // ) super.onCreate(savedInstanceState) + } - val messenger = flutterEngine!!.dartExecutor + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + + val messenger = flutterEngine.dartExecutor // notification: platform -> dart analysisStreamHandler = AnalysisStreamHandler().apply { @@ -99,6 +104,7 @@ open class MainActivity : FlutterActivity() { MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) + MethodChannel(messenger, SecurityHandler.CHANNEL).setMethodCallHandler(SecurityHandler(this)) MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this)) // - need ContextWrapper MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this)) @@ -172,7 +178,18 @@ open class MainActivity : FlutterActivity() { mediaSessionHandler.dispose() mediaStoreChangeStreamHandler.dispose() settingsChangeStreamHandler.dispose() - super.onDestroy() + try { + super.onDestroy() + } catch (e: Exception) { + // on Android 11, app may crash as follows: + // `Fatal Exception:` + // `java.lang.RuntimeException: Unable to destroy activity {deckers.thibault.aves/deckers.thibault.aves.MainActivity}:` + // `java.lang.IllegalArgumentException: NetworkCallback was not registered` + // related to this error: + // `Package android does not belong to 10162` + // cf https://issuetracker.google.com/issues/175055271 + Log.e(LOG_TAG, "failed while destroying activity", e) + } } override fun onNewIntent(intent: Intent) { @@ -182,6 +199,7 @@ open class MainActivity : FlutterActivity() { } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) when (requestCode) { DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(requestCode, resultCode, data) DELETE_SINGLE_PERMISSION_REQUEST, @@ -244,7 +262,7 @@ open class MainActivity : FlutterActivity() { Intent.ACTION_VIEW, Intent.ACTION_SEND, "com.android.camera.action.REVIEW" -> { (intent.data ?: intent.getParcelableExtraCompat(Intent.EXTRA_STREAM))?.let { uri -> // MIME type is optional - val type = intent.type ?: intent.resolveType(context) + val type = intent.type ?: intent.resolveType(this) return hashMapOf( INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW, INTENT_DATA_KEY_MIME_TYPE to type, @@ -314,7 +332,7 @@ open class MainActivity : FlutterActivity() { private fun submitPickedItems(call: MethodCall) { val pickedUris = call.argument>("uris") if (pickedUris != null && pickedUris.isNotEmpty()) { - val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(context, Uri.parse(uriString)) } + val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) } val intent = Intent().apply { val firstUri = toUri(pickedUris.first()) if (pickedUris.size == 1) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt index 4627eeb52..4429cd878 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt @@ -72,7 +72,10 @@ class SearchSuggestionsProvider : ContentProvider() { } } - val messenger = flutterEngine!!.dartExecutor + val engine = flutterEngine + engine ?: throw Exception("Flutter engine is not initialized") + + val messenger = engine.dartExecutor val backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply { setMethodCallHandler { call, result -> when (call.method) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt index a4285c3cf..202e73090 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt @@ -1,6 +1,5 @@ package deckers.thibault.aves -import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Build @@ -18,11 +17,12 @@ import deckers.thibault.aves.utils.FlutterUtils import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.getParcelableExtraCompat -import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.android.FlutterFragmentActivity +import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -class WallpaperActivity : FlutterActivity() { +class WallpaperActivity : FlutterFragmentActivity() { private lateinit var intentDataMap: MutableMap override fun onCreate(savedInstanceState: Bundle?) { @@ -36,8 +36,33 @@ class WallpaperActivity : FlutterActivity() { Log.i(LOG_TAG, "onCreate intent extras=$it") } intentDataMap = extractIntentData(intent) + } - initChannels(this) + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + val messenger = flutterEngine.dartExecutor + + // dart -> platform -> dart + // - need Context + MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this)) + MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this)) + MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this)) + MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this)) + MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) + MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this)) + // - need ContextWrapper + MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this)) + MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this)) + // - need Activity + MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this)) + + // result streaming: dart -> platform ->->-> dart + // - need Context + StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) } + + // intent handling + // detail fetch: dart -> platform + MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result -> onMethodCall(call, result) } } override fun onStart() { @@ -54,32 +79,6 @@ class WallpaperActivity : FlutterActivity() { } } - private fun initChannels(activity: Activity) { - val messenger = flutterEngine!!.dartExecutor - - // dart -> platform -> dart - // - need Context - MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(activity)) - MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(activity)) - MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(activity)) - MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(activity)) - MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(activity)) - MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(activity)) - // - need ContextWrapper - MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(activity)) - MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(activity)) - // - need Activity - MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(activity)) - - // result streaming: dart -> platform ->->-> dart - // - need Context - StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(activity, args) } - - // intent handling - // detail fetch: dart -> platform - MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result -> onMethodCall(call, result) } - } - private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "getIntentData" -> { @@ -94,7 +93,7 @@ class WallpaperActivity : FlutterActivity() { Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> { (intent.data ?: intent.getParcelableExtraCompat(Intent.EXTRA_STREAM))?.let { uri -> // MIME type is optional - val type = intent.type ?: intent.resolveType(context) + val type = intent.type ?: intent.resolveType(this) return hashMapOf( MainActivity.INTENT_DATA_KEY_ACTION to MainActivity.INTENT_ACTION_SET_WALLPAPER, MainActivity.INTENT_DATA_KEY_MIME_TYPE to type, 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 9f59bb879..f35eaaa39 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 @@ -21,7 +21,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler { when (call.method) { "canManageMedia" -> safe(call, result, ::canManageMedia) "getCapabilities" -> safe(call, result, ::getCapabilities) - "getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone) + "getDefaultTimeZoneRawOffsetMillis" -> safe(call, result, ::getDefaultTimeZoneRawOffsetMillis) "getLocales" -> safe(call, result, ::getLocales) "getPerformanceClass" -> safe(call, result, ::getPerformanceClass) "isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled) @@ -41,9 +41,10 @@ class DeviceHandler(private val context: Context) : MethodCallHandler { "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.LOLLIPOP), + "canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.M), "canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S), "canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N), + "canUseCrypto" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP), "hasGeocoder" to Geocoder.isPresent(), "isDynamicColorAvailable" to (sdkInt >= Build.VERSION_CODES.S), "showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O), @@ -52,8 +53,8 @@ class DeviceHandler(private val context: Context) : MethodCallHandler { ) } - private fun getDefaultTimeZone(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { - result.success(TimeZone.getDefault().id) + private fun getDefaultTimeZoneRawOffsetMillis(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + result.success(TimeZone.getDefault().rawOffset) } private fun getLocales(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt index e3373bcce..779213671 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt @@ -35,6 +35,15 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand } fun dispose() { + unregisterNoisyAudioReceiver() + } + + private fun registerNoisyAudioReceiver() { + context.registerReceiver(noisyAudioReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) + isNoisyAudioReceiverRegistered = true + } + + private fun unregisterNoisyAudioReceiver() { if (isNoisyAudioReceiverRegistered) { context.unregisterReceiver(noisyAudioReceiver) isNoisyAudioReceiverRegistered = false @@ -51,14 +60,17 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand private suspend fun updateSession(call: MethodCall, result: MethodChannel.Result) { val uri = call.argument("uri")?.let { Uri.parse(it) } - val title = call.argument("title") + val title = call.argument("title") ?: uri?.toString() val durationMillis = call.argument("durationMillis")?.toLong() val stateString = call.argument("state") val positionMillis = call.argument("positionMillis")?.toLong() val playbackSpeed = call.argument("playbackSpeed")?.toFloat() if (uri == null || title == null || durationMillis == null || stateString == null || positionMillis == null || playbackSpeed == null) { - result.error("update-args", "missing arguments", null) + result.error( + "updateSession-args", "missing arguments: uri=$uri, title=$title, durationMillis=$durationMillis" + + ", stateString=$stateString, positionMillis=$positionMillis, playbackSpeed=$playbackSpeed", null + ) return } @@ -67,7 +79,7 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand STATE_PAUSED -> PlaybackStateCompat.STATE_PAUSED STATE_PLAYING -> PlaybackStateCompat.STATE_PLAYING else -> { - result.error("update-state", "unknown state=$stateString", null) + result.error("updateSession-state", "unknown state=$stateString", null) return } } @@ -90,39 +102,41 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand .build() FlutterUtils.runOnUiThread { - if (session == null) { - val mbrIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE) - val mbrName = ComponentName(context, MediaButtonReceiver::class.java) - session = MediaSessionCompat(context, "aves", mbrName, mbrIntent).apply { - setCallback(mediaCommandHandler) + try { + if (session == null) { + val mbrIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE) + val mbrName = ComponentName(context, MediaButtonReceiver::class.java) + session = MediaSessionCompat(context, "aves", mbrName, mbrIntent).apply { + setCallback(mediaCommandHandler) + } } - } - session!!.apply { - val metadata = MediaMetadataCompat.Builder() - .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) - .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title) - .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis) - .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString()) - .build() - setMetadata(metadata) - setPlaybackState(playbackState) - if (!isActive) { - isActive = true + session!!.apply { + val metadata = MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) + .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title) + .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis) + .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString()) + .build() + setMetadata(metadata) + setPlaybackState(playbackState) + if (!isActive) { + isActive = true + } } - } - val isPlaying = state == PlaybackStateCompat.STATE_PLAYING - if (!wasPlaying && isPlaying) { - context.registerReceiver(noisyAudioReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) - isNoisyAudioReceiverRegistered = true - } else if (wasPlaying && !isPlaying) { - context.unregisterReceiver(noisyAudioReceiver) - isNoisyAudioReceiverRegistered = false + val isPlaying = state == PlaybackStateCompat.STATE_PLAYING + if (!wasPlaying && isPlaying) { + registerNoisyAudioReceiver() + } else if (wasPlaying && !isPlaying) { + unregisterNoisyAudioReceiver() + } + wasPlaying = isPlaying + + result.success(null) + } catch (e: Exception) { + result.error("updateSession-exception", e.message, e.stackTraceToString()) } - wasPlaying = isPlaying } - - result.success(null) } private fun releaseSession(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt index bb349855a..f8915a8f6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import java.io.FileNotFoundException class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCallHandler { private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) @@ -195,6 +196,8 @@ private class MetadataOpCallback( } else { "$errorCodeBase-mp4largeother" } + } else if (throwable is FileNotFoundException) { + "$errorCodeBase-filenotfound" } else { "$errorCodeBase-failure" } 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 291de3940..1358fdb69 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 @@ -67,7 +67,7 @@ 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.model.FieldMap -import deckers.thibault.aves.utils.ContextUtils.queryContentResolverProp +import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes.TIFF_EXTENSION_PATTERN @@ -104,8 +104,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { "getPanoramaInfo" -> ioScope.launch { safe(call, result, ::getPanoramaInfo) } "getIptc" -> ioScope.launch { safe(call, result, ::getIptc) } "getXmp" -> ioScope.launch { safe(call, result, ::getXmp) } - "hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentResolverProp) } - "getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentResolverProp) } + "hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentProp) } + "getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) } "getDate" -> ioScope.launch { safe(call, result, ::getDate) } "getDescription" -> ioScope.launch { safe(call, result, ::getDescription) } else -> result.notImplemented() @@ -1047,10 +1047,10 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { result.success(xmpStrings) } - private fun hasContentResolverProp(call: MethodCall, result: MethodChannel.Result) { + private fun hasContentProp(call: MethodCall, result: MethodChannel.Result) { val prop = call.argument("prop") if (prop == null) { - result.error("hasContentResolverProp-args", "missing arguments", null) + result.error("hasContentProp-args", "missing arguments", null) return } @@ -1058,27 +1058,27 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { when (prop) { "owner_package_name" -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q else -> { - result.error("hasContentResolverProp-unknown", "unknown property=$prop", null) + result.error("hasContentProp-unknown", "unknown property=$prop", null) return } } ) } - private fun getContentResolverProp(call: MethodCall, result: MethodChannel.Result) { + private fun getContentPropValue(call: MethodCall, result: MethodChannel.Result) { val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } val prop = call.argument("prop") if (mimeType == null || uri == null || prop == null) { - result.error("getContentResolverProp-args", "missing arguments", null) + result.error("getContentPropValue-args", "missing arguments", null) return } try { - val value = context.queryContentResolverProp(uri, mimeType, prop) + val value = context.queryContentPropValue(uri, mimeType, prop) result.success(value?.toString()) } catch (e: Exception) { - result.error("getContentResolverProp-query", "failed to query prop for uri=$uri", e.message) + result.error("getContentPropValue-query", "failed to query prop for uri=$uri", e.message) } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/SecurityHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/SecurityHandler.kt new file mode 100644 index 000000000..05a6ba398 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/SecurityHandler.kt @@ -0,0 +1,79 @@ +package deckers.thibault.aves.channel.calls + +import android.content.Context +import android.content.SharedPreferences +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +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 + +class SecurityHandler(private val context: Context) : MethodCallHandler { + private var sharedPreferences: SharedPreferences? = null + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "writeValue" -> safe(call, result, ::writeValue) + "readValue" -> safe(call, result, ::readValue) + else -> result.notImplemented() + } + } + + private fun getStore(): SharedPreferences { + if (sharedPreferences == null) { + val mainKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + sharedPreferences = EncryptedSharedPreferences.create( + context, + FILENAME, + mainKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + return sharedPreferences!! + } + + private fun writeValue(call: MethodCall, result: MethodChannel.Result) { + val key = call.argument("key") + val value = call.argument("value") + if (key == null) { + result.error("writeValue-args", "missing arguments", null) + return + } + + with(getStore().edit()) { + when (value) { + is Boolean -> putBoolean(key, value) + is Float -> putFloat(key, value) + is Int -> putInt(key, value) + is Long -> putLong(key, value) + is String -> putString(key, value) + null -> remove(key) + else -> { + result.error("writeValue-type", "unsupported type for value=$value", null) + return + } + } + apply() + } + result.success(true) + } + + private fun readValue(call: MethodCall, result: MethodChannel.Result) { + val key = call.argument("key") + if (key == null) { + result.error("readValue-args", "missing arguments", null) + return + } + + result.success(getStore().all[key]) + } + + companion object { + const val CHANNEL = "deckers.thibault/aves/security" + const val FILENAME = "secret_shared_prefs" + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt index 435705285..1dcc4bd32 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt @@ -8,6 +8,7 @@ import androidx.core.os.EnvironmentCompat import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.PermissionManager +import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils.getPrimaryVolumePath import deckers.thibault.aves.utils.StorageUtils.getVolumePaths import io.flutter.plugin.common.MethodCall @@ -25,6 +26,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "getStorageVolumes" -> ioScope.launch { safe(call, result, ::getStorageVolumes) } + "getVaultRoot" -> ioScope.launch { safe(call, result, ::getVaultRoot) } "getFreeSpace" -> ioScope.launch { safe(call, result, ::getFreeSpace) } "getGrantedDirectories" -> ioScope.launch { safe(call, result, ::getGrantedDirectories) } "getInaccessibleDirectories" -> ioScope.launch { safe(call, result, ::getInaccessibleDirectories) } @@ -88,6 +90,10 @@ class StorageHandler(private val context: Context) : MethodCallHandler { result.success(volumes) } + private fun getVaultRoot(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + result.success(StorageUtils.getVaultRoot(context)) + } + private fun getFreeSpace(call: MethodCall, result: MethodChannel.Result) { val path = call.argument("path") if (path == null) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt index 3086708ad..752633094 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt @@ -109,7 +109,7 @@ class ThumbnailFetcher internal constructor( } else { @Suppress("deprecation") var bitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null) - // from Android 10, returned thumbnail is already rotated according to EXIF orientation + // from Android 10 (API 29), returned thumbnail is already rotated according to EXIF orientation if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null) { bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped) } 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 258682b00..1f6590dd6 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 @@ -12,7 +12,7 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti result.success(true) } - override fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) { + private fun setWindowFlag(call: MethodCall, result: MethodChannel.Result, flag: Int) { val on = call.argument("on") if (on == null) { result.error("keepOn-args", "missing arguments", null) @@ -20,8 +20,6 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti } val window = activity.window - val flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON - val old = (window.attributes.flags and flag) != 0 if (old != on) { if (on) { @@ -33,6 +31,14 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti result.success(null) } + override fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) { + setWindowFlag(call, result, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + + override fun secureScreen(call: MethodCall, result: MethodChannel.Result) { + setWindowFlag(call, result, WindowManager.LayoutParams.FLAG_SECURE) + } + override fun requestOrientation(call: MethodCall, result: MethodChannel.Result) { val orientation = call.argument("orientation") if (orientation == null) { 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 46d1e43b8..3a50fd324 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 @@ -13,6 +13,10 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) { result.success(null) } + override fun secureScreen(call: MethodCall, result: MethodChannel.Result) { + result.success(null) + } + override fun requestOrientation(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 0a6f41249..492c6deeb 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 @@ -13,6 +13,7 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho when (call.method) { "isActivity" -> Coresult.safe(call, result, ::isActivity) "keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn) + "secureScreen" -> Coresult.safe(call, result, ::secureScreen) "isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked) "requestOrientation" -> Coresult.safe(call, result, ::requestOrientation) "isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware) @@ -25,6 +26,8 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho abstract fun keepScreenOn(call: MethodCall, result: MethodChannel.Result) + abstract fun secureScreen(call: MethodCall, result: MethodChannel.Result) + private fun isRotationLocked(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { var locked = false try { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt index 0e149942b..de0d7d78a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt @@ -139,8 +139,8 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments var destinationDir = arguments["destinationPath"] as String? val mimeType = arguments["mimeType"] as String? - val width = arguments["width"] as Int? - val height = arguments["height"] as Int? + val width = (arguments["width"] as Number?)?.toInt() + val height = (arguments["height"] as Number?)?.toInt() val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?) if (destinationDir == null || mimeType == null || width == null || height == null || nameConflictStrategy == null) { error("export-args", "missing arguments", null) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt index 74a9a9a3d..f618c0df1 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt @@ -15,7 +15,7 @@ import deckers.thibault.aves.metadata.Mp4ParserHelper.processBoxes import deckers.thibault.aves.metadata.Mp4ParserHelper.toBytes import deckers.thibault.aves.metadata.metadataextractor.SafeMp4UuidBoxHandler import deckers.thibault.aves.metadata.metadataextractor.SafeXmpReader -import deckers.thibault.aves.utils.ContextUtils.queryContentResolverProp +import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MemoryUtils import deckers.thibault.aves.utils.MimeTypes @@ -130,7 +130,7 @@ object XMP { ) { if (MimeTypes.isHeic(mimeType) && !foundXmp && StorageUtils.isMediaStoreContentUri(uri) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { try { - val xmpBytes = context.queryContentResolverProp(uri, mimeType, MediaStore.MediaColumns.XMP) + val xmpBytes = context.queryContentPropValue(uri, mimeType, MediaStore.MediaColumns.XMP) if (xmpBytes is ByteArray && xmpBytes.size > 0) { val xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes, SafeXmpReader.PARSE_OPTIONS) processXmp(xmpMeta) @@ -170,6 +170,11 @@ object XMP { } } // creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device` + + // TODO TLAD [mp4] `IsoFile` init may fail if a skipped box has a `org.mp4parser.boxes.iso14496.part12.MetaBox` as parent, + // because `MetaBox.parse()` changes the argument `dataSource` to a `RewindableReadableByteChannel`, + // so it is no longer a seekable `FileChannel`, which is a requirement to skip boxes. + IsoFile(channel, boxParser).use { isoFile -> isoFile.processBoxes(UserBox::class.java, true) { box, _ -> val boxSize = box.size 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 9d5749561..e858c03e4 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 @@ -33,6 +33,7 @@ import org.beyka.tiffbitmapfactory.TiffBitmapFactory import java.io.IOException class SourceEntry { + private val origin: Int val uri: Uri // content or file URI var path: String? = null // best effort to get local path private val sourceMimeType: String @@ -48,12 +49,14 @@ class SourceEntry { private var foundExif: Boolean = false - constructor(uri: Uri, sourceMimeType: String) { + constructor(origin: Int, uri: Uri, sourceMimeType: String) { + this.origin = origin this.uri = uri this.sourceMimeType = sourceMimeType } constructor(map: FieldMap) { + origin = map["origin"] as Int uri = Uri.parse(map["uri"] as String) path = map["path"] as String? sourceMimeType = map["sourceMimeType"] as String @@ -77,6 +80,7 @@ class SourceEntry { fun toMap(): FieldMap { return hashMapOf( + "origin" to origin, "uri" to uri.toString(), "path" to path, "sourceMimeType" to sourceMimeType, @@ -249,13 +253,15 @@ class SourceEntry { private fun fillByTiffDecode(context: Context) { try { - val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd() ?: return - val options = TiffBitmapFactory.Options().apply { - inJustDecodeBounds = true + context.contentResolver.openFileDescriptor(uri, "r")?.use { pfd -> + val fd = pfd.detachFd() + val options = TiffBitmapFactory.Options().apply { + inJustDecodeBounds = true + } + TiffBitmapFactory.decodeFileDescriptor(fd, options) + width = options.outWidth + height = options.outHeight } - TiffBitmapFactory.decodeFileDescriptor(fd, options) - width = options.outWidth - height = options.outHeight } catch (e: Exception) { // ignore } @@ -267,5 +273,11 @@ class SourceEntry { is Int -> o.toLong() else -> o as? Long } + + // should match `EntryOrigins` on the Dart side + const val ORIGIN_MEDIA_STORE_CONTENT = 0 + const val ORIGIN_UNKNOWN_CONTENT = 1 + const val ORIGIN_FILE = 2 + const val ORIGIN_VAULT = 3 } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt index 7ff773d21..368035721 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt @@ -44,6 +44,7 @@ internal class ContentImageProvider : ImageProvider() { } val fields: FieldMap = hashMapOf( + "origin" to SourceEntry.ORIGIN_UNKNOWN_CONTENT, "uri" to uri.toString(), "sourceMimeType" to mimeType, ) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt index 3c3a624d5..6a1b0725e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.ContextWrapper import android.net.Uri import android.util.Log +import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.SourceEntry import deckers.thibault.aves.utils.LogUtils import java.io.File @@ -15,7 +16,7 @@ internal class FileImageProvider : ImageProvider() { return } - val entry = SourceEntry(uri, sourceMimeType) + val entry = SourceEntry(SourceEntry.ORIGIN_FILE, uri, sourceMimeType) val path = uri.path if (path != null) { @@ -52,6 +53,19 @@ internal class FileImageProvider : ImageProvider() { throw Exception("failed to delete entry with uri=$uri path=$path") } + override fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: FieldMap, callback: ImageOpCallback) { + try { + val file = File(path) + if (file.exists()) { + newFields["dateModifiedSecs"] = file.lastModified() / 1000 + newFields["sizeBytes"] = file.length() + } + callback.onSuccess(newFields) + } catch (e: SecurityException) { + callback.onFailure(e) + } + } + companion object { private val LOG_TAG = LogUtils.createTag() } 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 9e2e9c9eb..4fb59c16c 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 @@ -811,11 +811,6 @@ abstract class ImageProvider { fields: List, callback: ImageOpCallback, ) { - if (dateMillis != null && dateMillis < 0) { - callback.onFailure(Exception("dateMillis=$dateMillis cannot be negative")) - return - } - val success = editExif(context, path, uri, mimeType, callback) { exif -> when { dateMillis != null -> { 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 3b29bcf55..5e568be50 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 @@ -9,6 +9,7 @@ import android.net.Uri import android.os.Build import android.provider.MediaStore import android.util.Log +import androidx.core.net.toUri import com.commonsware.cwac.document.DocumentFileCompat import deckers.thibault.aves.MainActivity import deckers.thibault.aves.MainActivity.Companion.DELETE_SINGLE_PERMISSION_REQUEST @@ -30,6 +31,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import java.io.File import java.io.OutputStream +import java.io.SyncFailedException import java.util.* import java.util.concurrent.CompletableFuture import kotlin.coroutines.Continuation @@ -219,6 +221,7 @@ class MediaStoreImageProvider : ImageProvider() { Log.w(LOG_TAG, "failed to make entry from uri=$itemUri because of null MIME type") } else { var entryMap: FieldMap = hashMapOf( + "origin" to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT, "uri" to itemUri.toString(), "path" to cursor.getString(pathColumn), "sourceMimeType" to mimeType, @@ -349,7 +352,7 @@ class MediaStoreImageProvider : ImageProvider() { } } catch (securityException: SecurityException) { // even if the app has access permission granted on the containing directory, - // the delete request may yield a `RecoverableSecurityException` on Android >=10 + // the delete request may yield a `RecoverableSecurityException` on API >=29 // when the underlying file no longer exists and this is an orphaned entry in the Media Store if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && contextWrapper is Activity) { Log.w(LOG_TAG, "caught a security exception when attempting to delete uri=$uri", securityException) @@ -386,10 +389,12 @@ class MediaStoreImageProvider : ImageProvider() { val entries = kv.value val toBin = targetDir == StorageUtils.TRASH_PATH_PLACEHOLDER + val toVault = StorageUtils.isInVault(activity, targetDir) + val toAppDir = toBin || toVault var effectiveTargetDir: String? = null var targetDirDocFile: DocumentFileCompat? = null - if (!toBin) { + if (!toAppDir) { effectiveTargetDir = targetDir targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir) if (!File(targetDir).exists()) { @@ -437,13 +442,20 @@ class MediaStoreImageProvider : ImageProvider() { // - there is no documentation regarding support for usage with removable storage // - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage try { - if (toBin) { - val trashDir = StorageUtils.trashDirFor(activity, sourcePath) - if (trashDir != null) { - effectiveTargetDir = ensureTrailingSeparator(trashDir.path) - targetDirDocFile = DocumentFileCompat.fromFile(trashDir) + val appDir = when { + toBin -> StorageUtils.trashDirFor(activity, sourcePath) + toVault -> File(targetDir) + else -> null + } + if (appDir != null) { + effectiveTargetDir = ensureTrailingSeparator(appDir.path) + targetDirDocFile = DocumentFileCompat.fromFile(appDir) + + if (toVault) { + appDir.mkdirs() } } + if (effectiveTargetDir != null) { val newFields = if (isCancelledOp()) skippedFieldMap else { val sourceFile = File(sourcePath) @@ -462,6 +474,7 @@ class MediaStoreImageProvider : ImageProvider() { mimeType = mimeType, copy = copy, toBin = toBin, + toVault = toVault, ) } } @@ -488,6 +501,7 @@ class MediaStoreImageProvider : ImageProvider() { mimeType: String, copy: Boolean, toBin: Boolean, + toVault: Boolean, ): FieldMap { val sourcePath = sourceFile.path val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) } @@ -512,7 +526,16 @@ class MediaStoreImageProvider : ImageProvider() { targetDir = targetDir, targetDirDocFile = targetDirDocFile, targetNameWithoutExtension = targetNameWithoutExtension, - ) { output: OutputStream -> sourceDocFile.copyTo(output) } + ) { output: OutputStream -> + try { + sourceDocFile.copyTo(output) + } catch (e: SyncFailedException) { + // The copied file is synced after writing, but it consistently fails in some cases + // (e.g. copying to SD card on Xiaomi 2201117PG with Android 11). + // It seems this failure can be safely ignored, as the new file is complete. + Log.w(LOG_TAG, "sync failure after copying from uri=$sourceUri, path=$sourcePath to targetDir=$targetDir", e) + } + } if (!copy) { // delete original entry @@ -522,13 +545,21 @@ class MediaStoreImageProvider : ImageProvider() { Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e) } } - if (toBin) { - return hashMapOf( + return if (toBin) { + hashMapOf( "trashed" to true, "trashPath" to targetPath, ) + } else if (toVault) { + hashMapOf( + "uri" to File(targetPath).toUri().toString(), + "contentId" to null, + "path" to targetPath, + "origin" to SourceEntry.ORIGIN_VAULT, + ) + } else { + scanNewPath(activity, targetPath, mimeType) } - return scanNewPath(activity, targetPath, mimeType) } // `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry @@ -910,7 +941,7 @@ class MediaStoreImageProvider : ImageProvider() { private val VIDEO_PROJECTION = arrayOf( *BASE_PROJECTION, MediaColumns.DURATION, - // `ORIENTATION` was only available for images before Android 10 + // `ORIENTATION` was only available for images before Android 10 (API 29) *if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) arrayOf( MediaStore.MediaColumns.ORIENTATION, ) else emptyArray() diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/ContextUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/ContextUtils.kt index a0683f388..df443830d 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/ContextUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/ContextUtils.kt @@ -7,6 +7,7 @@ import android.content.ContentUris import android.content.Context import android.database.Cursor import android.net.Uri +import android.provider.DocumentsContract import android.provider.MediaStore import android.util.Log import deckers.thibault.aves.utils.UriUtils.tryParseId @@ -30,7 +31,13 @@ object ContextUtils { return am.getRunningServices(Integer.MAX_VALUE).any { it.service.className == serviceClass.name } } - fun Context.queryContentResolverProp(uri: Uri, mimeType: String, prop: String): Any? { + // `flag`: `DocumentsContract.Document.FLAG_SUPPORTS_COPY`, etc. + fun Context.queryDocumentProviderFlag(docUri: Uri, flag: Int): Boolean { + val flags = queryContentPropValue(docUri, "", DocumentsContract.Document.COLUMN_FLAGS) as Long? + return if (flags != null) (flags.toInt() and flag) == flag else false + } + + fun Context.queryContentPropValue(uri: Uri, mimeType: String, column: String): Any? { var contentUri: Uri = uri if (StorageUtils.isMediaStoreContentUri(uri)) { uri.tryParseId()?.let { id -> @@ -43,26 +50,26 @@ object ContextUtils { } } - // throws SQLiteException when the requested prop is not a known column - val cursor = contentResolver.query(contentUri, arrayOf(prop), null, null, null) - if (cursor == null || !cursor.moveToFirst()) { - throw Exception("failed to get cursor for contentUri=$contentUri") - } - var value: Any? = null try { - value = when (cursor.getType(0)) { - Cursor.FIELD_TYPE_NULL -> null - Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0) - Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(0) - Cursor.FIELD_TYPE_STRING -> cursor.getString(0) - Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0) - else -> null + val cursor = contentResolver.query(contentUri, arrayOf(column), null, null, null) + if (cursor == null || !cursor.moveToFirst()) { + Log.w(LOG_TAG, "failed to get cursor for contentUri=$contentUri column=$column") + } else { + value = when (cursor.getType(0)) { + Cursor.FIELD_TYPE_NULL -> null + Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0) + Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(0) + Cursor.FIELD_TYPE_STRING -> cursor.getString(0) + Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0) + else -> null + } + cursor.close() } } catch (e: Exception) { - Log.w(LOG_TAG, "failed to get value for contentUri=$contentUri key=$prop", e) + // throws SQLiteException/IllegalArgumentException when the requested prop is not a known column + Log.w(LOG_TAG, "failed to get value for contentUri=$contentUri column=$column", e) } - cursor.close() return value } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt index df4ea5f75..1215d7ab7 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt @@ -1,5 +1,6 @@ package deckers.thibault.aves.utils +import android.webkit.MimeTypeMap import androidx.exifinterface.media.ExifInterface object MimeTypes { @@ -153,47 +154,11 @@ object MimeTypes { // among other refs: // - https://android.googlesource.com/platform/external/mime-support/+/refs/heads/master/mime.types fun extensionFor(mimeType: String): String? = when (mimeType) { - ARW -> ".arw" AVI, AVI_VND -> ".avi" - AVIF -> ".avif" - BMP -> ".bmp" - CR2 -> ".cr2" - CRW -> ".crw" - DCR -> ".dcr" - DJVU -> ".djvu" - DNG -> ".dng" - ERF -> ".erf" - GIF -> ".gif" HEIC, HEIF -> ".heif" - ICO -> ".ico" - JPEG -> ".jpg" - K25 -> ".k25" - KDC -> ".kdc" - MKV -> ".mkv" - MOV -> ".mov" MP2T, MP2TS -> ".m2ts" - MP4 -> ".mp4" - MRW -> ".mrw" - NEF -> ".nef" - NRW -> ".nrw" - OGV -> ".ogv" - ORF -> ".orf" - PEF -> ".pef" - PNG -> ".png" PSD_VND, PSD_X -> ".psd" - RAF -> ".raf" - RAW -> ".raw" - RW2 -> ".rw2" - SR2 -> ".sr2" - SRF -> ".srf" - SRW -> ".srw" - SVG -> ".svg" - TIFF -> ".tiff" - WBMP -> ".wbmp" - WEBM -> ".webm" - WEBP -> ".webp" - X3F -> ".x3f" - else -> null + else -> MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" } } val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE) 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 08355a5de..2bad51da4 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 @@ -119,7 +119,7 @@ object PermissionManager { dirSet.add("") } } else { - // request volume root until Android 10 + // request volume root until Android 10 (API 29) dirSet.add("") } dirsPerVolume[volumePath] = dirSet diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt index 078fda691..457a36e0e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt @@ -39,28 +39,29 @@ object StorageUtils { private const val TREE_URI_ROOT = "content://$EXTERNAL_STORAGE_PROVIDER_AUTHORITY/tree/" + private val UUID_PATTERN = Regex("[A-Fa-f\\d-]+") private val TREE_URI_PATH_PATTERN = Pattern.compile("(.*?):(.*)") const val TRASH_PATH_PLACEHOLDER = "#trash" private fun isAppFile(context: Context, path: String): Boolean { - val filesDirs = context.getExternalFilesDirs(null).filterNotNull() - return filesDirs.any { path.startsWith(it.path) } + val dirs = context.getExternalFilesDirs(null).filterNotNull() + return dirs.any { path.startsWith(it.path) } } private fun appExternalFilesDirFor(context: Context, path: String): File? { - val filesDirs = context.getExternalFilesDirs(null).filterNotNull() + val dirs = context.getExternalFilesDirs(null).filterNotNull() val volumePath = getVolumePath(context, path) - return volumePath?.let { filesDirs.firstOrNull { it.startsWith(volumePath) } } ?: filesDirs.firstOrNull() + return volumePath?.let { dirs.firstOrNull { it.startsWith(volumePath) } } ?: dirs.firstOrNull() } fun trashDirFor(context: Context, path: String): File? { - val filesDir = appExternalFilesDirFor(context, path) - if (filesDir == null) { + val externalFilesDir = appExternalFilesDirFor(context, path) + if (externalFilesDir == null) { Log.e(LOG_TAG, "failed to find external files dir for path=$path") return null } - val trashDir = File(filesDir, "trash") + val trashDir = File(externalFilesDir, "trash") if (!trashDir.exists() && !trashDir.mkdirs()) { Log.e(LOG_TAG, "failed to create directories at path=$trashDir") return null @@ -68,6 +69,10 @@ object StorageUtils { return trashDir } + fun getVaultRoot(context: Context) = ensureTrailingSeparator(File(context.filesDir, "vault").path) + + fun isInVault(context: Context, path: String) = path.startsWith(getVaultRoot(context)) + /** * Volume paths */ @@ -259,6 +264,7 @@ object StorageUtils { // e.g. // /storage/emulated/0/ -> primary // /storage/10F9-3F13/Pictures/ -> 10F9-3F13 + // /storage/extSdCard/ -> 1234-5678 [Android 5.1.1, Samsung Galaxy Core Prime] private fun getVolumeUuidForDocumentUri(context: Context, anyPath: String): String? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val sm = context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager @@ -278,7 +284,22 @@ object StorageUtils { return EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID } volumePath.split(File.separator).lastOrNull { it.isNotEmpty() }?.let { uuid -> - return uuid.uppercase(Locale.ROOT) + if (uuid.matches(UUID_PATTERN)) { + return uuid.uppercase(Locale.ROOT) + } + } + + // fallback when UUID does not appear in the SD card volume path + context.contentResolver.persistedUriPermissions.firstOrNull { uriPermission -> + convertTreeDocumentUriToDirPath(context, uriPermission.uri)?.let { + getVolumePath(context, it)?.let { grantedVolumePath -> + grantedVolumePath == volumePath + } + } ?: false + }?.let { uriPermission -> + splitTreeDocumentUri(uriPermission.uri)?.let { (uuid, _) -> + return uuid + } } } @@ -289,6 +310,7 @@ object StorageUtils { // e.g. // primary -> /storage/emulated/0/ // 10F9-3F13 -> /storage/10F9-3F13/ + // 1234-5678 -> /storage/extSdCard/ [Android 5.1.1, Samsung Galaxy Core Prime] private fun getVolumePathFromTreeDocumentUriUuid(context: Context, uuid: String): String? { if (uuid == EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID) { return getPrimaryVolumePath(context) @@ -318,6 +340,10 @@ object StorageUtils { } } + // fallback when UUID does not appear in the SD card volume path + val primaryVolumePath = getPrimaryVolumePath(context) + getVolumePaths(context).firstOrNull { it != primaryVolumePath }?.let { return it } + Log.e(LOG_TAG, "failed to find volume path for UUID=$uuid") return null } @@ -350,9 +376,9 @@ object StorageUtils { } // e.g. - // content://com.android.externalstorage.documents/tree/primary%3A -> /storage/emulated/0/ - // content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures -> /storage/10F9-3F13/Pictures/ - fun convertTreeDocumentUriToDirPath(context: Context, treeDocumentUri: Uri): String? { + // content://com.android.externalstorage.documents/tree/primary%3A -> ("primary", "") + // content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures -> ("10F9-3F13", "Pictures") + private fun splitTreeDocumentUri(treeDocumentUri: Uri): Pair? { val treeDocumentUriString = treeDocumentUri.toString() if (treeDocumentUriString.length <= TREE_URI_ROOT.length) return null val encoded = treeDocumentUriString.substring(TREE_URI_ROOT.length) @@ -362,13 +388,24 @@ object StorageUtils { val uuid = group(1) val relativePath = group(2) if (uuid != null && relativePath != null) { - val volumePath = getVolumePathFromTreeDocumentUriUuid(context, uuid) - if (volumePath != null) { - return ensureTrailingSeparator(volumePath + relativePath) - } + return Pair(uuid, relativePath) } } } + Log.e(LOG_TAG, "failed to split treeDocumentUri=$treeDocumentUri to UUID and relative path") + return null + } + + // e.g. + // content://com.android.externalstorage.documents/tree/primary%3A -> /storage/emulated/0/ + // content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures -> /storage/10F9-3F13/Pictures/ + fun convertTreeDocumentUriToDirPath(context: Context, treeDocumentUri: Uri): String? { + splitTreeDocumentUri(treeDocumentUri)?.let { (uuid, relativePath) -> + val volumePath = getVolumePathFromTreeDocumentUriUuid(context, uuid) + if (volumePath != null) { + return ensureTrailingSeparator(volumePath + relativePath) + } + } Log.e(LOG_TAG, "failed to convert treeDocumentUri=$treeDocumentUri to path") return null } @@ -512,7 +549,7 @@ object StorageUtils { } // As of Glide v4.12.0, a special loader `QMediaStoreUriLoader` is automatically used - // to work around a bug from Android 10 where metadata redaction corrupts HEIC images. + // to work around a bug from Android 10 (API 29) where metadata redaction corrupts HEIC images. // This loader relies on `MediaStore.setRequireOriginal` but this yields a `SecurityException` // for some non image/video content URIs (e.g. `downloads`, `file`) fun getGlideSafeUri(context: Context, uri: Uri, mimeType: String, sizeBytes: Long? = null): Uri { diff --git a/android/app/src/main/res/values-eu/strings.xml b/android/app/src/main/res/values-eu/strings.xml new file mode 100644 index 000000000..ca50b6ea6 --- /dev/null +++ b/android/app/src/main/res/values-eu/strings.xml @@ -0,0 +1,12 @@ + + + Bilatu + Bideoak + Argazki-markoa + Irudiak eta bideoak eskaneatu + Horma-papera + Media eskaneatu + Gelditu + Media eskaneatzen + Aves + \ No newline at end of file diff --git a/android/app/src/main/res/values-sk/strings.xml b/android/app/src/main/res/values-sk/strings.xml new file mode 100644 index 000000000..08fd7dbe9 --- /dev/null +++ b/android/app/src/main/res/values-sk/strings.xml @@ -0,0 +1,12 @@ + + + Hľadať + Aves + Rám fotky + Tapeta + 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/xml/data_extraction_rules.xml b/android/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 000000000..9f7329404 --- /dev/null +++ b/android/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/xml/full_backup_content.xml b/android/app/src/main/res/xml/full_backup_content.xml new file mode 100644 index 000000000..5e5ca09e7 --- /dev/null +++ b/android/app/src/main/res/xml/full_backup_content.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/android/app/src/main/res/xml/provider_paths.xml b/android/app/src/main/res/xml/provider_paths.xml index bdf6728a3..168574e97 100644 --- a/android/app/src/main/res/xml/provider_paths.xml +++ b/android/app/src/main/res/xml/provider_paths.xml @@ -1,7 +1,15 @@ + + + + + + diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index e0e447a3d..3a528264c 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/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.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/assets/terms.md b/assets/terms.md index 50e0dc078..dee3479b2 100644 --- a/assets/terms.md +++ b/assets/terms.md @@ -2,7 +2,7 @@ “Aves Gallery” is an open-source gallery and metadata explorer app allowing you to access and manage your local photos and videos. -You must use the app for legal, authorized and acceptable purposes. +The app is designed for legal, authorized and acceptable purposes. ## Disclaimer diff --git a/fastlane/metadata/android/en-US/changelogs/91.txt b/fastlane/metadata/android/en-US/changelogs/91.txt new file mode 100644 index 000000000..d1e8ac1c4 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/91.txt @@ -0,0 +1,5 @@ +In v1.8.0: +- Android TV support (cont'd) +- hide your secrets in vaults +- enjoy the app in Basque +Full changelog available on GitHub \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/9101.txt b/fastlane/metadata/android/en-US/changelogs/9101.txt new file mode 100644 index 000000000..d1e8ac1c4 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/9101.txt @@ -0,0 +1,5 @@ +In v1.8.0: +- Android TV support (cont'd) +- hide your secrets in vaults +- enjoy the app in Basque +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 new file mode 100644 index 000000000..b0d5229c3 --- /dev/null +++ b/fastlane/metadata/android/eu/full_description.txt @@ -0,0 +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. + +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 diff --git a/fastlane/metadata/android/eu/images/featureGraphic.png b/fastlane/metadata/android/eu/images/featureGraphic.png new file mode 100644 index 000000000..ad29dfe39 Binary files /dev/null and b/fastlane/metadata/android/eu/images/featureGraphic.png differ diff --git a/fastlane/metadata/android/eu/images/phoneScreenshots/1.png b/fastlane/metadata/android/eu/images/phoneScreenshots/1.png new file mode 100644 index 000000000..7f2dca48e Binary files /dev/null and b/fastlane/metadata/android/eu/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/eu/images/phoneScreenshots/2.png b/fastlane/metadata/android/eu/images/phoneScreenshots/2.png new file mode 100644 index 000000000..be2d319a7 Binary files /dev/null and b/fastlane/metadata/android/eu/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/eu/images/phoneScreenshots/3.png b/fastlane/metadata/android/eu/images/phoneScreenshots/3.png new file mode 100644 index 000000000..98fa51b08 Binary files /dev/null and b/fastlane/metadata/android/eu/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/eu/images/phoneScreenshots/4.png b/fastlane/metadata/android/eu/images/phoneScreenshots/4.png new file mode 100644 index 000000000..f2a53a498 Binary files /dev/null and b/fastlane/metadata/android/eu/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/eu/images/phoneScreenshots/5.png b/fastlane/metadata/android/eu/images/phoneScreenshots/5.png new file mode 100644 index 000000000..244028de4 Binary files /dev/null and b/fastlane/metadata/android/eu/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/eu/images/phoneScreenshots/6.png b/fastlane/metadata/android/eu/images/phoneScreenshots/6.png new file mode 100644 index 000000000..41a3c9e5b Binary files /dev/null and b/fastlane/metadata/android/eu/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/eu/images/phoneScreenshots/7.png b/fastlane/metadata/android/eu/images/phoneScreenshots/7.png new file mode 100644 index 000000000..3d99af3ed Binary files /dev/null and b/fastlane/metadata/android/eu/images/phoneScreenshots/7.png differ diff --git a/fastlane/metadata/android/eu/short_description.txt b/fastlane/metadata/android/eu/short_description.txt new file mode 100644 index 000000000..b2d28aa2e --- /dev/null +++ b/fastlane/metadata/android/eu/short_description.txt @@ -0,0 +1 @@ +Galeria eta metadatuen nabigatzailea \ No newline at end of file diff --git a/fastlane/metadata/android/sk/full_description.txt b/fastlane/metadata/android/sk/full_description.txt new file mode 100644 index 000000000..74557d170 --- /dev/null +++ b/fastlane/metadata/android/sk/full_description.txt @@ -0,0 +1,5 @@ +Aves aplikácia je schopná si poradiť s veľkým množstvom rôznych formátov obrázkov a videí, ako napríklad typickými JPEG a MP4, ale aj s exotickejšími TIFF, SVG, AVI a mnoho iných! Aplikácia skenuje média v zariadení a identifikje fotky v pohybe, panorámy , 360° videá, ako aj GeoTIFF súbory. + +Navigácia a vyhľadávanie je dôležitou súčasťou aplikácie Aves. Jej cieľom je poskytnúť užívateľom jednoduchý prechod z albumov, do fotiek, tagov, máp, atď. + +Aves je schopný pracovať s Android (od KitKat do Android 13, včetne Android TV) a ponúka rozšírenia ako miniaplikácie (widgety), skratky aplikácie, šetrič obrazovky a globálne vyhľadávanie. Rovnako poskytuje prehľadávnie médií. \ No newline at end of file diff --git a/fastlane/metadata/android/sk/short_description.txt b/fastlane/metadata/android/sk/short_description.txt new file mode 100644 index 000000000..fa44ffa8c --- /dev/null +++ b/fastlane/metadata/android/sk/short_description.txt @@ -0,0 +1 @@ +Prehliadač galérie a metadát \ No newline at end of file diff --git a/lib/app_mode.dart b/lib/app_mode.dart index de3cd6455..925c3d967 100644 --- a/lib/app_mode.dart +++ b/lib/app_mode.dart @@ -26,6 +26,11 @@ extension ExtraAppMode on AppMode { bool get canSelectFilter => this == AppMode.main; + bool get canCreateFilter => { + AppMode.main, + AppMode.pickFilterInternal, + }.contains(this); + bool get isPickingMedia => { AppMode.pickSingleMediaExternal, AppMode.pickMultipleMediaExternal, diff --git a/lib/geo/countries.dart b/lib/geo/countries.dart index 8f1e24025..f2b0c32b6 100644 --- a/lib/geo/countries.dart +++ b/lib/geo/countries.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:isolate'; import 'package:aves/geo/topojson.dart'; import 'package:collection/collection.dart'; @@ -48,26 +49,22 @@ class CountryTopology { final topology = await getTopology(); if (topology == null) return null; - return compute<_IsoNumericCodeMapData, Map>>(_isoNumericCodeMap, _IsoNumericCodeMapData(topology, positions)); - } - - static Future>> _isoNumericCodeMap(_IsoNumericCodeMapData data) async { try { - final topology = data.topology; - final countries = (topology.objects['countries'] as GeometryCollection).geometries; - final byCode = >{}; - for (final position in data.positions) { - final code = _getNumeric(topology, countries, position); - if (code != null) { - byCode[code] = (byCode[code] ?? {})..add(position); + return Isolate.run>>(() { + final countries = (topology.objects['countries'] as GeometryCollection).geometries; + final byCode = >{}; + for (final position in positions) { + final code = _getNumeric(topology, countries, position); + if (code != null) { + byCode[code] = (byCode[code] ?? {})..add(position); + } } - } - return byCode; + return byCode; + }); } catch (error, stack) { - // an unhandled error in a spawn isolate would make the app crash debugPrint('failed to get country codes with error=$error\n$stack'); + return null; } - return {}; } static int? _getNumeric(Topology topology, List mruCountries, LatLng position) { @@ -96,10 +93,3 @@ class CountryTopology { return null; } } - -class _IsoNumericCodeMapData { - Topology topology; - Set positions; - - _IsoNumericCodeMapData(this.topology, this.positions); -} diff --git a/lib/geo/topojson.dart b/lib/geo/topojson.dart index c61dc4b73..de9961cd4 100644 --- a/lib/geo/topojson.dart +++ b/lib/geo/topojson.dart @@ -1,24 +1,22 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:isolate'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; // cf https://github.com/topojson/topojson-specification class TopoJson { - Future parse(String data) async { - return compute(_isoParse, data); - } - - static Topology? _isoParse(String jsonData) { + Future parse(String jsonData) async { try { - final data = jsonDecode(jsonData) as Map; - return Topology.parse(data); + return Isolate.run(() { + final data = jsonDecode(jsonData) as Map; + return Topology.parse(data); + }); } catch (error, stack) { - // an unhandled error in a spawn isolate would make the app crash debugPrint('failed to parse TopoJSON with error=$error\n$stack'); + return null; } - return null; } } diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index b487cf3b0..a33a9887c 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -44,19 +44,19 @@ } } }, - "timeDays": "{days, plural, =1{1 den} =2..4{{days} dny} other{{days} dnů}}", + "timeDays": "{days, plural, =1{1 den} few{{days} dny} other{{days} dnů}}", "@timeDays": { "placeholders": { "days": {} } }, - "itemCount": "{count, plural, =1{1 položka} =2..4{{count} položky} other{{count} položek}}", + "itemCount": "{count, plural, =1{1 položka} few{{count} položky} other{{count} položek}}", "@itemCount": { "placeholders": { "count": {} } }, - "columnCount": "{count, plural, =1{1 sloupec} =2..4{{count} sloupce} other{{count} sloupců}}", + "columnCount": "{count, plural, =1{1 sloupec} few{{count} sloupce} other{{count} sloupců}}", "@columnCount": { "placeholders": { "count": {} @@ -168,8 +168,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "Rychlost přehrávání", "@videoActionSetSpeed": {}, - "videoActionSettings": "Nastavení", - "@videoActionSettings": {}, + "viewerActionSettings": "Nastavení", + "@viewerActionSettings": {}, "slideshowActionResume": "Pokračovat", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Zobrazit ve sbírce", @@ -313,7 +313,7 @@ "@videoPlaybackMuted": {}, "videoPlaybackWithSound": "Přehrát se zvukem", "@videoPlaybackWithSound": {}, - "themeBrightnessLight": "Svetlé", + "themeBrightnessLight": "Světlé", "@themeBrightnessLight": {}, "themeBrightnessDark": "Tmavé", "@themeBrightnessDark": {}, @@ -436,7 +436,7 @@ "@addShortcutButtonLabel": {}, "noMatchingAppDialogMessage": "Pro tuto operaci není k dispozici žádná aplikace.", "@noMatchingAppDialogMessage": {}, - "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Smazat tuto položku?} =2..4{Smazat tyto {count} položky?} other{Smazat těchto {count} položek?}}", + "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Smazat tuto položku?} few{Smazat tyto {count} položky?} other{Smazat těchto {count} položek?}}", "@deleteEntriesConfirmationDialogMessage": { "placeholders": { "count": {} @@ -485,13 +485,13 @@ "@renameEntrySetPagePreviewSectionTitle": {}, "renameProcessorName": "Název", "@renameProcessorName": {}, - "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Smazat toto album a tuto položku?} =2..4{Smazat toto album a tyto {count} položky?} other{Smazat toto album a těchto {count} položek?}}", + "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Smazat toto album a v něm obsaženou položku?} few{Smazat toto album a v něm obsažené {count} položky?} other{Smazat toto album a v něm obsažených {count} položek?}}", "@deleteSingleAlbumConfirmationDialogMessage": { "placeholders": { "count": {} } }, - "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Smazat tato alba a jejich položku?} =2..4{Smazat tato alba a jejich {count} položky?} other{Smazat tato alba a jejich {count} položek?}}", + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Smazat tato alba a v nich obsaženou položku?} few{Smazat tato alba a v nich obsažené {count} položky?} other{Smazat tato alba a v nich obsažených {count} položek?}}", "@deleteMultiAlbumConfirmationDialogMessage": { "placeholders": { "count": {} @@ -955,7 +955,7 @@ "@nameConflictStrategySkip": {}, "keepScreenOnNever": "Nikdy", "@keepScreenOnNever": {}, - "binEntriesConfirmationDialogMessage": "{count, plural, =1{Přesunout tuto položku do koše?} =2..4{Přesunout tyto {count} položky do koše?} other{Přesunout těchto {count} položek do koše?}}", + "binEntriesConfirmationDialogMessage": "{count, plural, =1{Přesunout tuto položku do koše?} few{Přesunout tyto {count} položky do koše?} other{Přesunout těchto {count} položek do koše?}}", "@binEntriesConfirmationDialogMessage": { "placeholders": { "count": {} @@ -1121,25 +1121,25 @@ "count": {} } }, - "collectionCopySuccessFeedback": "{count, plural, =1{Zkopírována 1 položka} =2..4{Zkopírovány {count} položky} other{Zkopírováno {count} položek}}", + "collectionCopySuccessFeedback": "{count, plural, =1{Zkopírována 1 položka} few{Zkopírovány {count} položky} other{Zkopírováno {count} položek}}", "@collectionCopySuccessFeedback": { "placeholders": { "count": {} } }, - "collectionMoveSuccessFeedback": "{count, plural, =1{Přesunuta 1 položka} =2..4{Přesunuty {count} položky} other{Přesunuto {count} položek}}", + "collectionMoveSuccessFeedback": "{count, plural, =1{Přesunuta 1 položka} few{Přesunuty {count} položky} other{Přesunuto {count} položek}}", "@collectionMoveSuccessFeedback": { "placeholders": { "count": {} } }, - "collectionRenameSuccessFeedback": "{count, plural, =1{Přejmenována 1 položka} =2..4{Přejmenovány {count} položky} other{Přejmenováno {count} položek}}", + "collectionRenameSuccessFeedback": "{count, plural, =1{Přejmenována 1 položka} few{Přejmenovány {count} položky} other{Přejmenováno {count} položek}}", "@collectionRenameSuccessFeedback": { "placeholders": { "count": {} } }, - "collectionEditSuccessFeedback": "{count, plural, =1{Upravena 1 položka} =2..4{Upraveny {count} položky} other{Upraveno {count} položek}}", + "collectionEditSuccessFeedback": "{count, plural, =1{Upravena 1 položka} few{Upraveny {count} položky} other{Upraveno {count} položek}}", "@collectionEditSuccessFeedback": { "placeholders": { "count": {} @@ -1209,8 +1209,6 @@ "@albumGroupNone": {}, "albumVideoCaptures": "Snímky videa", "@albumVideoCaptures": {}, - "createAlbumTooltip": "Vytvořit album", - "@createAlbumTooltip": {}, "countryPageTitle": "Země", "@countryPageTitle": {}, "searchCollectionFieldHint": "Prohledat sbírky", @@ -1283,13 +1281,13 @@ "@settingsThumbnailShowVideoDuration": {}, "settingsCollectionQuickActionsTile": "Rychlé akce", "@settingsCollectionQuickActionsTile": {}, - "timeMinutes": "{minutes, plural, =1{1 minuta} =2..4{{minutes} minuty} other{{minutes} minut}}", + "timeMinutes": "{minutes, plural, =1{1 minuta} few{{minutes} minuty} other{{minutes} minut}}", "@timeMinutes": { "placeholders": { "minutes": {} } }, - "timeSeconds": "{seconds, plural, =1{1 sekunda} =2..4{{seconds} sekundy} other{{seconds} sekund}}", + "timeSeconds": "{seconds, plural, =1{1 sekunda} few{{seconds} sekundy} other{{seconds} sekund}}", "@timeSeconds": { "placeholders": { "seconds": {} @@ -1331,7 +1329,7 @@ "@settingsStorageAccessBanner": {}, "settingsUnitSystemTile": "Jednotky", "@settingsUnitSystemTile": {}, - "statsWithGps": "{count, plural, =1{1 položka s polohou} =2..4{{count} položky s polohou} other{{count} položek s polohou}}", + "statsWithGps": "{count, plural, =1{1 položka s polohou} few{{count} položky s polohou} other{{count} položek s polohou}}", "@statsWithGps": { "placeholders": { "count": {} @@ -1354,5 +1352,19 @@ "mapAttributionStamen": "Mapová data © [OpenStreetMap](https://www.openstreetmap.org/copyright) přispěvatelé • Dlaždice z [Stamen Design](https://stamen.com), [CC BY 3.0](https://creativecommons.org/licenses/by/3.0)", "@mapAttributionStamen": {}, "panoramaDisableSensorControl": "Zakázat ovládání senzorem", - "@panoramaDisableSensorControl": {} + "@panoramaDisableSensorControl": {}, + "filterLocatedLabel": "S polohou", + "@filterLocatedLabel": {}, + "filterTaggedLabel": "Se štítky", + "@filterTaggedLabel": {}, + "settingsDisplayUseTvInterface": "Rozhraní Android TV", + "@settingsDisplayUseTvInterface": {}, + "tooManyItemsErrorDialogMessage": "Zkuste znovu s několika dalšími položkami.", + "@tooManyItemsErrorDialogMessage": {}, + "settingsModificationWarningDialogMessage": "Ostatní nastavení budou upravena.", + "@settingsModificationWarningDialogMessage": {}, + "settingsViewerShowDescription": "Zobrazit popis", + "@settingsViewerShowDescription": {}, + "settingsVideoGestureVerticalDragBrightnessVolume": "Potáhnout nahoru či dolů pro úpravu jasu/hlasitosti", + "@settingsVideoGestureVerticalDragBrightnessVolume": {} } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 3b32f7b1a..16361c294 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -151,8 +151,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "Wiedergabegeschwindigkeit", "@videoActionSetSpeed": {}, - "videoActionSettings": "Einstellungen", - "@videoActionSettings": {}, + "viewerActionSettings": "Einstellungen", + "@viewerActionSettings": {}, "slideshowActionResume": "Wiedergabe", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "In Sammlung anzeigen", @@ -389,9 +389,9 @@ "@renameProcessorCounter": {}, "renameProcessorName": "Name", "@renameProcessorName": {}, - "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Sicher, dass dieses Album und der Inhalt gelöscht werden soll?} other{Sicher, dass dieses Album und deren {count} Elemente gelöscht werden sollen?}}", + "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Das Album und das darin enthaltene Element löschen?} other{Das Album und die {count} darin enthaltenen Elemente löschen?}}", "@deleteSingleAlbumConfirmationDialogMessage": {}, - "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Sicher, dass diese Alben und deren Inhalt gelöscht werden sollen?} other{Sicher, dass diese Alben und deren {count} Elemente gelöscht werden sollen?}}", + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Diese Alben und die darin enthaltenen Elemente löschen?} other{Diese Alben und die darin enthaltenen {count} Objekte löschen?}}", "@deleteMultiAlbumConfirmationDialogMessage": {}, "exportEntryDialogFormat": "Format:", "@exportEntryDialogFormat": {}, @@ -595,7 +595,7 @@ "@collectionCopySuccessFeedback": {}, "collectionMoveSuccessFeedback": "{count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}", "@collectionMoveSuccessFeedback": {}, - "collectionRenameSuccessFeedback": "{count, plural, =1{1 Element unmebannt} other{{count} Elemente umbenannt}}", + "collectionRenameSuccessFeedback": "{count, plural, =1{1 Element umbenannt} other{{count} Elemente umbenannt}}", "@collectionRenameSuccessFeedback": {}, "collectionEditSuccessFeedback": "{count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}", "@collectionEditSuccessFeedback": {}, @@ -699,8 +699,6 @@ "@albumPageTitle": {}, "albumEmpty": "Keine Alben", "@albumEmpty": {}, - "createAlbumTooltip": "Album erstellen", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "ERSTELLE", "@createAlbumButtonLabel": {}, "newFilterBanner": "Neu", @@ -1188,5 +1186,27 @@ "entryInfoActionRemoveLocation": "Standort entfernen", "@entryInfoActionRemoveLocation": {}, "entryActionShareVideoOnly": "Nur das Video teilen", - "@entryActionShareVideoOnly": {} + "@entryActionShareVideoOnly": {}, + "filterLocatedLabel": "Mit Standort", + "@filterLocatedLabel": {}, + "filterTaggedLabel": "Getaggt", + "@filterTaggedLabel": {}, + "settingsAccessibilityShowPinchGestureAlternatives": "Alternativen für Multi-Touch-Gesten anzeigen", + "@settingsAccessibilityShowPinchGestureAlternatives": {}, + "settingsDisplayUseTvInterface": "Android-TV Oberfläche", + "@settingsDisplayUseTvInterface": {}, + "columnCount": "{count, plural, =1{1 Spalte} other{{count} Spalten}}", + "@columnCount": { + "placeholders": { + "count": {} + } + }, + "settingsVideoGestureVerticalDragBrightnessVolume": "Nach oben oder unten wischen, um die Helligkeit/Lautstärke einzustellen", + "@settingsVideoGestureVerticalDragBrightnessVolume": {}, + "tooManyItemsErrorDialogMessage": "Noch einmal mit weniger Elementen versuchen.", + "@tooManyItemsErrorDialogMessage": {}, + "settingsModificationWarningDialogMessage": "Andere Einstellungen werden angepasst.", + "@settingsModificationWarningDialogMessage": {}, + "settingsViewerShowDescription": "Beschreibung anzeigen", + "@settingsViewerShowDescription": {} } diff --git a/lib/l10n/app_el.arb b/lib/l10n/app_el.arb index 6214117be..3bf1a4af2 100644 --- a/lib/l10n/app_el.arb +++ b/lib/l10n/app_el.arb @@ -151,8 +151,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "Ταχύτητα αναπαραγωγής", "@videoActionSetSpeed": {}, - "videoActionSettings": "Ρυθμίσεις", - "@videoActionSettings": {}, + "viewerActionSettings": "Ρυθμίσεις", + "@viewerActionSettings": {}, "slideshowActionResume": "Συνέχιση", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Εμφάνιση στη Συλλογή", @@ -699,8 +699,6 @@ "@albumPageTitle": {}, "albumEmpty": "Δεν υπάρχουν άλμπουμ", "@albumEmpty": {}, - "createAlbumTooltip": "Δημιουργία άλμπουμ", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "ΔΗΜΙΟΥΡΓΙΑ", "@createAlbumButtonLabel": {}, "newFilterBanner": "Νέα", @@ -1206,5 +1204,49 @@ "settingsDisplayUseTvInterface": "Χρήση του Android TV περιβάλλον", "@settingsDisplayUseTvInterface": {}, "settingsViewerShowDescription": "Εμφάνιση περιγραφής", - "@settingsViewerShowDescription": {} + "@settingsViewerShowDescription": {}, + "chipActionLock": "Κλείδωμα", + "@chipActionLock": {}, + "chipActionCreateVault": "Δημιουργήστε θησαυροφυλάκιο", + "@chipActionCreateVault": {}, + "chipActionConfigureVault": "Διαμορφώστε το θησαυροφυλάκιο", + "@chipActionConfigureVault": {}, + "albumTierVaults": "Θησαυροφυλακια", + "@albumTierVaults": {}, + "vaultLockTypePassword": "Κωδικός πρόσβασης", + "@vaultLockTypePassword": {}, + "newVaultDialogTitle": "Νεο Θησαυροφυλακιο", + "@newVaultDialogTitle": {}, + "configureVaultDialogTitle": "Διαμορφωστε το θησαυροφυλακιο", + "@configureVaultDialogTitle": {}, + "vaultDialogLockModeWhenScreenOff": "Κλείδωμα όταν σβήνει η οθόνη", + "@vaultDialogLockModeWhenScreenOff": {}, + "vaultDialogLockTypeLabel": "Τύπος κλειδώματος", + "@vaultDialogLockTypeLabel": {}, + "pinDialogConfirm": "Επιβεβαιώστε το PIN", + "@pinDialogConfirm": {}, + "passwordDialogEnter": "Εισάγετε τον κωδικό πρόσβασης", + "@passwordDialogEnter": {}, + "passwordDialogConfirm": "Επιβεβαιώστε τον κωδικό πρόσβασης", + "@passwordDialogConfirm": {}, + "authenticateToUnlockVault": "Πιστοποίηση ταυτότητας για να ξεκλειδώσετε το θησαυροφυλάκιο", + "@authenticateToUnlockVault": {}, + "settingsConfirmationVaultDataLoss": "Εμφάνιση προειδοποίησης απώλειας δεδομένων θησαυροφυλακίου", + "@settingsConfirmationVaultDataLoss": {}, + "authenticateToConfigureVault": "Πιστοποίηση ταυτότητας για τη διαμόρφωση του θησαυροφυλακίου", + "@authenticateToConfigureVault": {}, + "vaultLockTypePin": "PIN", + "@vaultLockTypePin": {}, + "newVaultWarningDialogMessage": "Τα αρχεία στα θησαυροφυλάκια είναι διαθέσιμα μόνο σε αυτή την εφαρμογή και σε καμία άλλη.\n\nΑν απεγκαταστήσετε την εφαρμογή ή έστω διαγράψετε τα δεδομένα της εφαρμογής, θα χάσετε όλα σας τα κρυφά αρχεία.", + "@newVaultWarningDialogMessage": {}, + "pinDialogEnter": "Εισάγετε το PIN", + "@pinDialogEnter": {}, + "vaultBinUsageDialogMessage": "Ορισμένα θησαυροφυλάκια χρησιμοποιούν τον κάδο ανακύκλωσης.", + "@vaultBinUsageDialogMessage": {}, + "settingsDisablingBinWarningDialogMessage": "Τα αρχεία στον κάδο ανακύκλωσης θα διαγραφούν για πάντα.", + "@settingsDisablingBinWarningDialogMessage": {}, + "tooManyItemsErrorDialogMessage": "Δοκιμάστε ξανά με λιγότερα αρχεία.", + "@tooManyItemsErrorDialogMessage": {}, + "settingsVideoGestureVerticalDragBrightnessVolume": "Σύρετε προς τα πάνω ή προς τα κάτω για να ρυθμίσετε τη φωτεινότητα/την ένταση του ήχου", + "@settingsVideoGestureVerticalDragBrightnessVolume": {} } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1141b9bfb..7104192c1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -78,11 +78,14 @@ "chipActionFilterOut": "Filter out", "chipActionFilterIn": "Filter in", "chipActionHide": "Hide", + "chipActionLock": "Lock", "chipActionPin": "Pin to top", "chipActionUnpin": "Unpin from top", "chipActionRename": "Rename", "chipActionSetCover": "Set cover", "chipActionCreateAlbum": "Create album", + "chipActionCreateVault": "Create vault", + "chipActionConfigureVault": "Configure vault", "entryActionCopyToClipboard": "Copy to clipboard", "entryActionDelete": "Delete", @@ -119,7 +122,8 @@ "videoActionSkip10": "Seek forward 10 seconds", "videoActionSelectStreams": "Select tracks", "videoActionSetSpeed": "Playback speed", - "videoActionSettings": "Settings", + + "viewerActionSettings": "Settings", "slideshowActionResume": "Resume", "slideshowActionShowInCollection": "Show in Collection", @@ -157,6 +161,16 @@ "filterMimeImageLabel": "Image", "filterMimeVideoLabel": "Video", + "accessibilityAnimationsRemove": "Prevent screen effects", + "accessibilityAnimationsKeep": "Keep screen effects", + + "albumTierNew": "New", + "albumTierPinned": "Pinned", + "albumTierSpecial": "Common", + "albumTierApps": "Apps", + "albumTierVaults": "Vaults", + "albumTierRegular": "Others", + "coordinateFormatDms": "DMS", "coordinateFormatDecimal": "Decimal degrees", "coordinateDms": "{coordinate} {direction}", @@ -177,17 +191,13 @@ "coordinateDmsEast": "E", "coordinateDmsWest": "W", - "unitSystemMetric": "Metric", - "unitSystemImperial": "Imperial", + "displayRefreshRatePreferHighest": "Highest rate", + "displayRefreshRatePreferLowest": "Lowest rate", - "videoLoopModeNever": "Never", - "videoLoopModeShortOnly": "Short videos only", - "videoLoopModeAlways": "Always", - - "videoControlsPlay": "Play", - "videoControlsPlaySeek": "Play & seek backward/forward", - "videoControlsPlayOutside": "Open with other player", - "videoControlsNone": "None", + "keepScreenOnNever": "Never", + "keepScreenOnVideoPlayback": "During video playback", + "keepScreenOnViewerOnly": "Viewer page only", + "keepScreenOnAlways": "Always", "mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleHybrid": "Google Maps (Hybrid)", @@ -202,28 +212,32 @@ "nameConflictStrategyReplace": "Replace", "nameConflictStrategySkip": "Skip", - "keepScreenOnNever": "Never", - "keepScreenOnVideoPlayback": "During video playback", - "keepScreenOnViewerOnly": "Viewer page only", - "keepScreenOnAlways": "Always", - - "accessibilityAnimationsRemove": "Prevent screen effects", - "accessibilityAnimationsKeep": "Keep screen effects", - - "displayRefreshRatePreferHighest": "Highest rate", - "displayRefreshRatePreferLowest": "Lowest rate", - "subtitlePositionTop": "Top", "subtitlePositionBottom": "Bottom", - "videoPlaybackSkip": "Skip", - "videoPlaybackMuted": "Play muted", - "videoPlaybackWithSound": "Play with sound", - "themeBrightnessLight": "Light", "themeBrightnessDark": "Dark", "themeBrightnessBlack": "Black", + "unitSystemMetric": "Metric", + "unitSystemImperial": "Imperial", + + "vaultLockTypePin": "Pin", + "vaultLockTypePassword": "Password", + + "videoControlsPlay": "Play", + "videoControlsPlaySeek": "Play & seek backward/forward", + "videoControlsPlayOutside": "Open with other player", + "videoControlsNone": "None", + + "videoLoopModeNever": "Never", + "videoLoopModeShortOnly": "Short videos only", + "videoLoopModeAlways": "Always", + + "videoPlaybackSkip": "Skip", + "videoPlaybackMuted": "Play muted", + "videoPlaybackWithSound": "Play with sound", + "viewerTransitionSlide": "Slide", "viewerTransitionParallax": "Parallax", "viewerTransitionFade": "Fade", @@ -241,12 +255,6 @@ "widgetOpenPageCollection": "Open collection", "widgetOpenPageViewer": "Open viewer", - "albumTierNew": "New", - "albumTierPinned": "Pinned", - "albumTierSpecial": "Common", - "albumTierApps": "Apps", - "albumTierRegular": "Others", - "storageVolumeDescriptionFallbackPrimary": "Internal storage", "storageVolumeDescriptionFallbackNonPrimary": "SD card", "rootDirectoryDescription": "root directory", @@ -366,6 +374,23 @@ "newAlbumDialogNameLabelAlreadyExistsHelper": "Directory already exists", "newAlbumDialogStorageLabel": "Storage:", + "newVaultWarningDialogMessage": "Items in vaults are only available to this app and no others.\n\nIf you uninstall this app, or clear this app data, you will lose all these items.", + "newVaultDialogTitle": "New Vault", + "configureVaultDialogTitle": "Configure Vault", + "vaultDialogLockModeWhenScreenOff": "Lock when screen turns off", + "vaultDialogLockTypeLabel": "Lock type", + + "pinDialogEnter": "Enter pin", + "pinDialogConfirm": "Confirm pin", + + "passwordDialogEnter": "Enter password", + "passwordDialogConfirm": "Confirm password", + + "authenticateToConfigureVault": "Authenticate to configure vault", + "authenticateToUnlockVault": "Authenticate to unlock vault", + + "vaultBinUsageDialogMessage": "Some vaults are using the recycle bin.", + "renameAlbumDialogLabel": "New name", "renameAlbumDialogLabelAlreadyExistsHelper": "Directory already exists", @@ -634,7 +659,6 @@ "albumPageTitle": "Albums", "albumEmpty": "No albums", - "createAlbumTooltip": "Create album", "createAlbumButtonLabel": "CREATE", "newFilterBanner": "new", @@ -687,6 +711,7 @@ "settingsConfirmationBeforeMoveToBinItems": "Ask before moving items to the recycle bin", "settingsConfirmationBeforeMoveUndatedItems": "Ask before moving undated items", "settingsConfirmationAfterMoveToBinItems": "Show message after moving items to the recycle bin", + "settingsConfirmationVaultDataLoss": "Show vault data loss warning", "settingsNavigationDrawerTile": "Navigation menu", "settingsNavigationDrawerEditorPageTitle": "Navigation Menu", @@ -790,6 +815,7 @@ "settingsSaveSearchHistory": "Save search history", "settingsEnableBin": "Use recycle bin", "settingsEnableBinSubtitle": "Keep deleted items for 30 days", + "settingsDisablingBinWarningDialogMessage": "Items in the recycle bin will be deleted forever.", "settingsAllowMediaManagement": "Allow media management", "settingsHiddenItemsTile": "Hidden items", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index e4609e5fd..abe773a66 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -147,8 +147,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "Velocidad de reproducción", "@videoActionSetSpeed": {}, - "videoActionSettings": "Ajustes", - "@videoActionSettings": {}, + "viewerActionSettings": "Ajustes", + "@viewerActionSettings": {}, "slideshowActionResume": "Reanudar", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Mostrar en Colección", @@ -657,8 +657,6 @@ "@albumPageTitle": {}, "albumEmpty": "Sin álbumes", "@albumEmpty": {}, - "createAlbumTooltip": "Crear álbum", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "CREAR", "@createAlbumButtonLabel": {}, "newFilterBanner": "nuevo", @@ -1210,5 +1208,45 @@ "tooManyItemsErrorDialogMessage": "Vuelva a intentarlo con menos elementos.", "@tooManyItemsErrorDialogMessage": {}, "settingsVideoGestureVerticalDragBrightnessVolume": "Deslice hacia arriba o hacia abajo para ajustar el brillo o el volumen", - "@settingsVideoGestureVerticalDragBrightnessVolume": {} + "@settingsVideoGestureVerticalDragBrightnessVolume": {}, + "chipActionLock": "Bloquear", + "@chipActionLock": {}, + "chipActionConfigureVault": "Configurar la caja fuerte", + "@chipActionConfigureVault": {}, + "albumTierVaults": "Caja fuerte", + "@albumTierVaults": {}, + "vaultLockTypePin": "Pin", + "@vaultLockTypePin": {}, + "vaultLockTypePassword": "Contraseña", + "@vaultLockTypePassword": {}, + "newVaultDialogTitle": "Nueva caja fuerte", + "@newVaultDialogTitle": {}, + "configureVaultDialogTitle": "Configurar la caja fuerte", + "@configureVaultDialogTitle": {}, + "vaultDialogLockModeWhenScreenOff": "Bloquear al apagar la pantalla", + "@vaultDialogLockModeWhenScreenOff": {}, + "vaultDialogLockTypeLabel": "Tipo de bloqueo", + "@vaultDialogLockTypeLabel": {}, + "pinDialogEnter": "Introducir el código pin", + "@pinDialogEnter": {}, + "pinDialogConfirm": "Confirmar el código pin", + "@pinDialogConfirm": {}, + "passwordDialogEnter": "Introducir la contraseña", + "@passwordDialogEnter": {}, + "passwordDialogConfirm": "Confirmar la contraseña", + "@passwordDialogConfirm": {}, + "authenticateToConfigureVault": "Autenticarse para configurar la caja fuerte", + "@authenticateToConfigureVault": {}, + "vaultBinUsageDialogMessage": "Algunas cajas fuertes utilizan la papelera de reciclaje.", + "@vaultBinUsageDialogMessage": {}, + "settingsDisablingBinWarningDialogMessage": "Los elementos de la papelera de reciclaje se borrarán para siempre.", + "@settingsDisablingBinWarningDialogMessage": {}, + "chipActionCreateVault": "Crear una caja fuerte", + "@chipActionCreateVault": {}, + "newVaultWarningDialogMessage": "Los elementos de la caja fuerte sólo están disponibles para esta aplicación y no para otras.\n\nSi desinstalas esta aplicación o borras sus datos, perderás todos estos elementos.", + "@newVaultWarningDialogMessage": {}, + "authenticateToUnlockVault": "Autentificarse para desbloquear la caja fuerte", + "@authenticateToUnlockVault": {}, + "settingsConfirmationVaultDataLoss": "Mostrar un aviso de pérdida de datos de la caja fuerte", + "@settingsConfirmationVaultDataLoss": {} } diff --git a/lib/l10n/app_eu.arb b/lib/l10n/app_eu.arb new file mode 100644 index 000000000..f5c4f7854 --- /dev/null +++ b/lib/l10n/app_eu.arb @@ -0,0 +1,1370 @@ +{ + "saveTooltip": "Gorde", + "@saveTooltip": {}, + "columnCount": "{count, plural, =1{zutabe 1} other{{count} zutabe}}", + "@columnCount": { + "placeholders": { + "count": {} + } + }, + "timeSeconds": "{seconds, plural, =1{segundu 1} other{{seconds} segundu}}", + "@timeSeconds": { + "placeholders": { + "seconds": {} + } + }, + "timeMinutes": "{minutes, plural, =1{minutu 1} other{{minutes} minutu}}", + "@timeMinutes": { + "placeholders": { + "minutes": {} + } + }, + "timeDays": "{days, plural, =1{egun 1} other{{days} egun}}", + "@timeDays": { + "placeholders": { + "days": {} + } + }, + "focalLength": "{length} mm", + "@focalLength": { + "placeholders": { + "length": { + "type": "String", + "example": "5.4" + } + } + }, + "applyButtonLabel": "APLIKATU", + "@applyButtonLabel": {}, + "deleteButtonLabel": "EZABATU", + "@deleteButtonLabel": {}, + "nextButtonLabel": "HURRENGOA", + "@nextButtonLabel": {}, + "showButtonLabel": "ERAKUTSI", + "@showButtonLabel": {}, + "hideButtonLabel": "EZKUTATU", + "@hideButtonLabel": {}, + "appName": "Aves", + "@appName": {}, + "welcomeMessage": "Ongi etorri", + "@welcomeMessage": {}, + "welcomeOptional": "Aukerazkoa", + "@welcomeOptional": {}, + "itemCount": "{count, plural, =1{elementu 1} other{{count} elementu}}", + "@itemCount": { + "placeholders": { + "count": {} + } + }, + "welcomeTermsToggle": "Termino eta baldintzekin ados nago", + "@welcomeTermsToggle": {}, + "cancelTooltip": "Utzi", + "@cancelTooltip": {}, + "continueButtonLabel": "JARRAITU", + "@continueButtonLabel": {}, + "nextTooltip": "Hurrengoa", + "@nextTooltip": {}, + "changeTooltip": "Aldatu", + "@changeTooltip": {}, + "clearTooltip": "Garbitu", + "@clearTooltip": {}, + "previousTooltip": "Aurrekoa", + "@previousTooltip": {}, + "showTooltip": "Erakutsi", + "@showTooltip": {}, + "actionRemove": "Ezabatu", + "@actionRemove": {}, + "resetTooltip": "Berrezarri", + "@resetTooltip": {}, + "hideTooltip": "Ezkutatu", + "@hideTooltip": {}, + "doubleBackExitMessage": "Sakatu «itzuli» berriro irteteko.", + "@doubleBackExitMessage": {}, + "pickTooltip": "Aukeratu", + "@pickTooltip": {}, + "chipActionGoToAlbumPage": "Erakutsi albumetan", + "@chipActionGoToAlbumPage": {}, + "chipActionGoToTagPage": "Erakutsi etiketetan", + "@chipActionGoToTagPage": {}, + "entryActionRotateCW": "Biratu erlojuaren orratzen noranzkoan", + "@entryActionRotateCW": {}, + "entryActionPrint": "Inprimatu", + "@entryActionPrint": {}, + "entryActionConvertMotionPhotoToStillImage": "Bihurtu irudi finko batean", + "@entryActionConvertMotionPhotoToStillImage": {}, + "coordinateDmsSouth": "H", + "@coordinateDmsSouth": {}, + "coordinateDmsEast": "E", + "@coordinateDmsEast": {}, + "nameConflictStrategyRename": "Berrizendatu", + "@nameConflictStrategyRename": {}, + "videoPlaybackSkip": "Jauzi", + "@videoPlaybackSkip": {}, + "videoPlaybackWithSound": "Soinuarekin erreproduzitu", + "@videoPlaybackWithSound": {}, + "viewerTransitionFade": "Desagertu", + "@viewerTransitionFade": {}, + "storageVolumeDescriptionFallbackNonPrimary": "SD txartela", + "@storageVolumeDescriptionFallbackNonPrimary": {}, + "rootDirectoryDescription": "Erro-karpeta", + "@rootDirectoryDescription": {}, + "otherDirectoryDescription": "«{name}» karpeta", + "@otherDirectoryDescription": { + "placeholders": { + "name": { + "type": "String", + "example": "Pictures", + "description": "the name of a specific directory" + } + } + }, + "doNotAskAgain": "Ez galdetu berriro", + "@doNotAskAgain": {}, + "chipActionDelete": "Ezabatu", + "@chipActionDelete": {}, + "sourceStateLoading": "Kargatzen", + "@sourceStateLoading": {}, + "sourceStateCataloguing": "Katalogatzen", + "@sourceStateCataloguing": {}, + "sourceStateLocatingCountries": "Herrialdeak kokatzen", + "@sourceStateLocatingCountries": {}, + "sourceStateLocatingPlaces": "Lekuak kokatzen", + "@sourceStateLocatingPlaces": {}, + "chipActionGoToCountryPage": "Erakutsi herrialdeetan", + "@chipActionGoToCountryPage": {}, + "chipActionFilterIn": "Iragazi", + "@chipActionFilterIn": {}, + "chipActionFilterOut": "Ez iragazi", + "@chipActionFilterOut": {}, + "chipActionHide": "Ezkutatu", + "@chipActionHide": {}, + "chipActionPin": "Finkatu goran", + "@chipActionPin": {}, + "entryInfoActionRemoveMetadata": "Kendu metadatuak", + "@entryInfoActionRemoveMetadata": {}, + "slideshowActionResume": "Jarraitu", + "@slideshowActionResume": {}, + "slideshowActionShowInCollection": "Erakutsi bilduman", + "@slideshowActionShowInCollection": {}, + "entryInfoActionEditTags": "Editatu etiketak", + "@entryInfoActionEditTags": {}, + "entryInfoActionEditLocation": "Editatu kokapena", + "@entryInfoActionEditLocation": {}, + "entryInfoActionExportMetadata": "Esportatu metadatuak", + "@entryInfoActionExportMetadata": {}, + "filterAspectRatioPortraitLabel": "Formatu bertikala", + "@filterAspectRatioPortraitLabel": {}, + "entryInfoActionRemoveLocation": "Ezabatu kokapena", + "@entryInfoActionRemoveLocation": {}, + "filterAspectRatioLandscapeLabel": "Formatu horizontala", + "@filterAspectRatioLandscapeLabel": {}, + "filterBinLabel": "Zakarrontzia", + "@filterBinLabel": {}, + "filterFavouriteLabel": "Gogokoa", + "@filterFavouriteLabel": {}, + "filterNoAddressLabel": "Helbiderik ez", + "@filterNoAddressLabel": {}, + "filterNoDateLabel": "Datarik gabe", + "@filterNoDateLabel": {}, + "entryActionExport": "Esportatu", + "@entryActionExport": {}, + "chipActionUnpin": "Desfinkatu goitik", + "@chipActionUnpin": {}, + "entryActionRename": "Aldatu izena", + "@entryActionRename": {}, + "chipActionRename": "Aldatu izena", + "@chipActionRename": {}, + "entryActionDelete": "Ezabatu", + "@entryActionDelete": {}, + "entryActionInfo": "Informazioa", + "@entryActionInfo": {}, + "chipActionSetCover": "Ezarri azala", + "@chipActionSetCover": {}, + "chipActionCreateAlbum": "Sortu albuma", + "@chipActionCreateAlbum": {}, + "entryActionCopyToClipboard": "Kopiatu arbelera", + "@entryActionCopyToClipboard": {}, + "entryActionConvert": "Bihurtu", + "@entryActionConvert": {}, + "entryActionFlip": "Buelta eman horizontalki", + "@entryActionFlip": {}, + "entryActionRestore": "Berrezarri", + "@entryActionRestore": {}, + "entryActionRotateCCW": "Biratu erlojuaren orratzen kontrako noranzkoan", + "@entryActionRotateCCW": {}, + "entryActionShare": "Partekatu", + "@entryActionShare": {}, + "filterNoRatingLabel": "Kalifikaziorik gabe", + "@filterNoRatingLabel": {}, + "filterTaggedLabel": "Etiketatua", + "@filterTaggedLabel": {}, + "filterNoTitleLabel": "Izenbururik gabe", + "@filterNoTitleLabel": {}, + "filterLocatedLabel": "Kokatua", + "@filterLocatedLabel": {}, + "filterNoTagLabel": "Etiketatu gabe", + "@filterNoTagLabel": {}, + "filterTypeAnimatedLabel": "Animatua", + "@filterTypeAnimatedLabel": {}, + "filterNoLocationLabel": "Kokatu gabe", + "@filterNoLocationLabel": {}, + "filterRatingRejectedLabel": "Baztertua", + "@filterRatingRejectedLabel": {}, + "filterRecentlyAddedLabel": "Duela gutxi gehitua", + "@filterRecentlyAddedLabel": {}, + "filterOnThisDayLabel": "Egun honetan", + "@filterOnThisDayLabel": {}, + "filterTypeMotionPhotoLabel": "Argazki-mugikorra", + "@filterTypeMotionPhotoLabel": {}, + "filterTypeSphericalVideoLabel": "360°-ko bideoa", + "@filterTypeSphericalVideoLabel": {}, + "filterMimeImageLabel": "Irudi", + "@filterMimeImageLabel": {}, + "filterTypePanoramaLabel": "Panoramika", + "@filterTypePanoramaLabel": {}, + "filterTypeRawLabel": "Raw", + "@filterTypeRawLabel": {}, + "filterTypeGeotiffLabel": "GeoTIFF", + "@filterTypeGeotiffLabel": {}, + "filterMimeVideoLabel": "Bideo", + "@filterMimeVideoLabel": {}, + "entryActionShareImageOnly": "Partekatu irudia soilik", + "@entryActionShareImageOnly": {}, + "entryActionShareVideoOnly": "Partekatu bideoa soilik", + "@entryActionShareVideoOnly": {}, + "entryActionViewSource": "Ikusi iturria", + "@entryActionViewSource": {}, + "unitSystemMetric": "Metriko", + "@unitSystemMetric": {}, + "videoControlsPlay": "Erreproduzitu", + "@videoControlsPlay": {}, + "entryActionShowGeoTiffOnMap": "Erakutsi gainjarritako mapa bezala", + "@entryActionShowGeoTiffOnMap": {}, + "coordinateFormatDms": "DMS (Dokumentuak kudeatzeko sistema)", + "@coordinateFormatDms": {}, + "coordinateDmsNorth": "I", + "@coordinateDmsNorth": {}, + "coordinateDmsWest": "M", + "@coordinateDmsWest": {}, + "coordinateFormatDecimal": "Gradu hamartarrak", + "@coordinateFormatDecimal": {}, + "coordinateDms": "{coordinate} {direction}", + "@coordinateDms": { + "placeholders": { + "coordinate": { + "type": "String", + "example": "38° 41′ 47.72″" + }, + "direction": { + "type": "String", + "example": "S" + } + } + }, + "unitSystemImperial": "Inperial", + "@unitSystemImperial": {}, + "videoLoopModeAlways": "Beti", + "@videoLoopModeAlways": {}, + "videoControlsPlayOutside": "Ireki beste erreproduzitzaile batekin", + "@videoControlsPlayOutside": {}, + "videoLoopModeNever": "Inoiz", + "@videoLoopModeNever": {}, + "videoLoopModeShortOnly": "Bideo laburrak soilik", + "@videoLoopModeShortOnly": {}, + "videoControlsPlaySeek": "Erreproduzitu eta aurrera edo atzera egin", + "@videoControlsPlaySeek": {}, + "mapStyleGoogleNormal": "Google Maps", + "@mapStyleGoogleNormal": {}, + "nameConflictStrategySkip": "Jauzi", + "@nameConflictStrategySkip": {}, + "videoControlsNone": "Bat ere ez", + "@videoControlsNone": {}, + "mapStyleGoogleHybrid": "Google Maps (higikorra)", + "@mapStyleGoogleHybrid": {}, + "mapStyleGoogleTerrain": "Google Maps (lurra)", + "@mapStyleGoogleTerrain": {}, + "mapStyleHuaweiNormal": "Petal Maps", + "@mapStyleHuaweiNormal": {}, + "mapStyleOsmHot": "OSM humanitarioa", + "@mapStyleOsmHot": {}, + "mapStyleStamenWatercolor": "Stamen Watercolor (akuarela)", + "@mapStyleStamenWatercolor": {}, + "keepScreenOnNever": "Inoiz", + "@keepScreenOnNever": {}, + "mapStyleHuaweiTerrain": "Petal Maps (lurra)", + "@mapStyleHuaweiTerrain": {}, + "nameConflictStrategyReplace": "Ordezkatu", + "@nameConflictStrategyReplace": {}, + "mapStyleStamenToner": "Stamen Toner (monokromatikoa)", + "@mapStyleStamenToner": {}, + "keepScreenOnVideoPlayback": "Bideoa erreproduzitzean", + "@keepScreenOnVideoPlayback": {}, + "keepScreenOnAlways": "Beti", + "@keepScreenOnAlways": {}, + "keepScreenOnViewerOnly": "Bisorea soilik", + "@keepScreenOnViewerOnly": {}, + "subtitlePositionTop": "Goran", + "@subtitlePositionTop": {}, + "accessibilityAnimationsKeep": "Pantaila-efektuak mantendu", + "@accessibilityAnimationsKeep": {}, + "displayRefreshRatePreferLowest": "Kalifikazio baxuena", + "@displayRefreshRatePreferLowest": {}, + "accessibilityAnimationsRemove": "Pantaila-efektuak ekidin", + "@accessibilityAnimationsRemove": {}, + "displayRefreshRatePreferHighest": "Kalifikazio gorena", + "@displayRefreshRatePreferHighest": {}, + "entryActionViewMotionPhotoVideo": "Ireki bideoa", + "@entryActionViewMotionPhotoVideo": {}, + "entryActionEdit": "Editatu", + "@entryActionEdit": {}, + "entryActionOpenMap": "Erakutsi mapako aplikazioan", + "@entryActionOpenMap": {}, + "entryActionRotateScreen": "Biratu pantaila", + "@entryActionRotateScreen": {}, + "entryActionOpen": "Ireki honekin", + "@entryActionOpen": {}, + "entryActionSetAs": "Ezarri hau bezala", + "@entryActionSetAs": {}, + "subtitlePositionBottom": "Behean", + "@subtitlePositionBottom": {}, + "themeBrightnessLight": "Argi", + "@themeBrightnessLight": {}, + "videoPlaybackMuted": "Mutututa erreproduzitu", + "@videoPlaybackMuted": {}, + "themeBrightnessBlack": "Beltz", + "@themeBrightnessBlack": {}, + "themeBrightnessDark": "Ilun", + "@themeBrightnessDark": {}, + "viewerTransitionSlide": "Diapositiba", + "@viewerTransitionSlide": {}, + "entryActionAddFavourite": "Gehitu gogokoetara", + "@entryActionAddFavourite": {}, + "entryActionRemoveFavourite": "Kendu gogokoetatik", + "@entryActionRemoveFavourite": {}, + "viewerTransitionParallax": "Paralaxi", + "@viewerTransitionParallax": {}, + "viewerTransitionNone": "Bat ere ez", + "@viewerTransitionNone": {}, + "viewerTransitionZoomIn": "Hurbildu", + "@viewerTransitionZoomIn": {}, + "wallpaperTargetHome": "Hasierako pantaila", + "@wallpaperTargetHome": {}, + "wallpaperTargetLock": "Blokeo-pantaila", + "@wallpaperTargetLock": {}, + "wallpaperTargetHomeLock": "Hasiera- eta blokeo-pantaila", + "@wallpaperTargetHomeLock": {}, + "widgetDisplayedItemRandom": "Ausazko", + "@widgetDisplayedItemRandom": {}, + "widgetDisplayedItemMostRecent": "Berrienak", + "@widgetDisplayedItemMostRecent": {}, + "albumTierRegular": "Besteak", + "@albumTierRegular": {}, + "videoActionUnmute": "Ez mututu", + "@videoActionUnmute": {}, + "widgetOpenPageHome": "Ireki hasierako pantaila", + "@widgetOpenPageHome": {}, + "widgetOpenPageCollection": "Ireki bilduma", + "@widgetOpenPageCollection": {}, + "widgetOpenPageViewer": "Ireki bisorea", + "@widgetOpenPageViewer": {}, + "albumTierNew": "Berria", + "@albumTierNew": {}, + "albumTierPinned": "Finkatua", + "@albumTierPinned": {}, + "albumTierSpecial": "Ohiko", + "@albumTierSpecial": {}, + "albumTierApps": "Aplikazioak", + "@albumTierApps": {}, + "storageVolumeDescriptionFallbackPrimary": "Barne-biltegiratzea", + "@storageVolumeDescriptionFallbackPrimary": {}, + "videoActionCaptureFrame": "Atzitu fotograma", + "@videoActionCaptureFrame": {}, + "videoActionMute": "Mututu", + "@videoActionMute": {}, + "videoActionSelectStreams": "Hautatu pistak", + "@videoActionSelectStreams": {}, + "entryInfoActionEditRating": "Editatu balorazioa", + "@entryInfoActionEditRating": {}, + "videoActionSkip10": "Aurrera 10 segundu egin", + "@videoActionSkip10": {}, + "videoActionPause": "Eten", + "@videoActionPause": {}, + "videoActionPlay": "Erreproduzitu", + "@videoActionPlay": {}, + "videoActionReplay10": "Atzera 10 segundu egin", + "@videoActionReplay10": {}, + "viewerActionSettings": "Ezarpenak", + "@viewerActionSettings": {}, + "entryInfoActionEditDate": "Editatu data eta ordua", + "@entryInfoActionEditDate": {}, + "entryInfoActionEditTitleDescription": "Editatu izenburua eta deskribapena", + "@entryInfoActionEditTitleDescription": {}, + "videoActionSetSpeed": "Erreproduzitze-abiadura", + "@videoActionSetSpeed": {}, + "storageAccessDialogMessage": "Mesedez, «{volume}»-(e)n {directory} aukera ezazu hurrengo pantailan aplikazio honi sarrera baimentzeko.", + "@storageAccessDialogMessage": { + "placeholders": { + "directory": { + "type": "String", + "description": "the name of a directory, using the output of `rootDirectoryDescription` or `otherDirectoryDescription`" + }, + "volume": { + "type": "String", + "example": "SD card", + "description": "the name of a storage volume" + } + } + }, + "restrictedAccessDialogMessage": "Aplikazio honek ez du baimenik «{volume}»-(e)ko {directory}-(e)n fitxategiak aldatzeko.\n\nMesedez, aurreinstalatutako fitxategi-kudeatzaile edo galeriaren aplikazio bat erabili itemak beste karpeta batera mugitzeko.", + "@restrictedAccessDialogMessage": { + "placeholders": { + "directory": { + "type": "String", + "description": "the name of a directory, using the output of `rootDirectoryDescription` or `otherDirectoryDescription`" + }, + "volume": { + "type": "String", + "example": "SD card", + "description": "the name of a storage volume" + } + } + }, + "nameConflictDialogSingleSourceMessage": "Helburuko karpetan dauden fitxategi batzuek izen bera dute.", + "@nameConflictDialogSingleSourceMessage": {}, + "binEntriesConfirmationDialogMessage": "{count, plural, =1{Elementu hau zakarrontzira bidali nahi al duzu?} other{{count} elementu hauek zakarrontzira bidali nahi al dituzu?}}", + "@binEntriesConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "addShortcutDialogLabel": "Laster-bidearen etiketa", + "@addShortcutDialogLabel": {}, + "unsupportedTypeDialogMessage": "{count, plural, =1{Eragiketa hau ez da bateragarria aukeratutako hurrengo elementu-motarentzako: {types}.} other{Eragiketa hau ez da bateragarria aukeratutako hurrengo elementu-motentzako: {types}.}}", + "@unsupportedTypeDialogMessage": { + "placeholders": { + "count": {}, + "types": { + "type": "String", + "example": "GIF, TIFF, MP4", + "description": "a list of unsupported types" + } + } + }, + "moveUndatedConfirmationDialogMessage": "Elementuen datak gorde nahi al dituzu aurrera egin aurretik?", + "@moveUndatedConfirmationDialogMessage": {}, + "moveUndatedConfirmationDialogSetDate": "Gorde datak", + "@moveUndatedConfirmationDialogSetDate": {}, + "videoResumeDialogMessage": "Erreprodukzioa {time} denboran jarraitzea nahi al duzu?", + "@videoResumeDialogMessage": { + "placeholders": { + "time": { + "type": "String", + "example": "13:37" + } + } + }, + "nameConflictDialogMultipleSourceMessage": "Fitxategi batzuek izen bera dute.", + "@nameConflictDialogMultipleSourceMessage": {}, + "addShortcutButtonLabel": "GEHITU", + "@addShortcutButtonLabel": {}, + "videoStartOverButtonLabel": "BERRIRO HASI", + "@videoStartOverButtonLabel": {}, + "missingSystemFilePickerDialogMessage": "Sistemako fitxategien bilatzailea falta da edo desaktibatua dago. Mesedez, aktiba ezazu eta saia zaitez berriro.", + "@missingSystemFilePickerDialogMessage": {}, + "noMatchingAppDialogMessage": "Hau maneiatu dezakeen aplikaziorik ez dago.", + "@noMatchingAppDialogMessage": {}, + "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Elementu hau ezaba nahi al duzu?} other{{count} elementu hauek ezaba nahi al dituzu?}}", + "@deleteEntriesConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "videoResumeButtonLabel": "JARRAITU", + "@videoResumeButtonLabel": {}, + "setCoverDialogLatest": "Azkeneko elementua", + "@setCoverDialogLatest": {}, + "setCoverDialogAuto": "Automatikoa", + "@setCoverDialogAuto": {}, + "setCoverDialogCustom": "Pertsonalizatua", + "@setCoverDialogCustom": {}, + "newAlbumDialogTitle": "Album berria", + "@newAlbumDialogTitle": {}, + "newAlbumDialogNameLabel": "Albumaren izena", + "@newAlbumDialogNameLabel": {}, + "newAlbumDialogNameLabelAlreadyExistsHelper": "Karpeta jada badago", + "@newAlbumDialogNameLabelAlreadyExistsHelper": {}, + "newAlbumDialogStorageLabel": "Biltegiratzea:", + "@newAlbumDialogStorageLabel": {}, + "renameAlbumDialogLabel": "Izen berria", + "@renameAlbumDialogLabel": {}, + "renameAlbumDialogLabelAlreadyExistsHelper": "Direktorioa jada badago", + "@renameAlbumDialogLabelAlreadyExistsHelper": {}, + "renameEntrySetPageTitle": "Aldatu izena", + "@renameEntrySetPageTitle": {}, + "renameEntrySetPagePatternFieldLabel": "Izendapen-eredua", + "@renameEntrySetPagePatternFieldLabel": {}, + "renameEntrySetPageInsertTooltip": "Txertatu eremua", + "@renameEntrySetPageInsertTooltip": {}, + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Album hauek eta hauetan dagoen elementua ezaba nahi al dituzu?} other{Album hauek eta hauetan dauden {count} elementuak ezaba nahi al dituzu?}}", + "@deleteMultiAlbumConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "exportEntryDialogFormat": "Formatua:", + "@exportEntryDialogFormat": {}, + "exportEntryDialogWidth": "Zabalera", + "@exportEntryDialogWidth": {}, + "exportEntryDialogHeight": "Altuera", + "@exportEntryDialogHeight": {}, + "renameEntryDialogLabel": "Izen berria", + "@renameEntryDialogLabel": {}, + "editEntryDialogCopyFromItem": "Kopiatu beste elementu batetik", + "@editEntryDialogCopyFromItem": {}, + "editEntryDialogTargetFieldsHeader": "Aldatzeko eremuak", + "@editEntryDialogTargetFieldsHeader": {}, + "editEntryDateDialogTitle": "Data eta ordua", + "@editEntryDateDialogTitle": {}, + "editEntryDateDialogSetCustom": "Ezarri data pertsonalizatua", + "@editEntryDateDialogSetCustom": {}, + "editEntryDateDialogCopyField": "Kopiatu beste data batetik", + "@editEntryDateDialogCopyField": {}, + "editEntryDateDialogExtractFromTitle": "Atera izenburutik", + "@editEntryDateDialogExtractFromTitle": {}, + "editEntryDateDialogShift": "Aldatu", + "@editEntryDateDialogShift": {}, + "editEntryLocationDialogLongitude": "Longitudea", + "@editEntryLocationDialogLongitude": {}, + "locationPickerUseThisLocationButton": "Erabili kokapen hau", + "@locationPickerUseThisLocationButton": {}, + "hideFilterConfirmationDialogMessage": "Bat-etortzen diren argazki eta bideoak zure bildumatik ezkutatuko dira. Berriro agerrarazi ditzakezu «Pribatutasuna» ezarpenetatik.\n\nZiur al zaude ezkuta nahi al dituzula?", + "@hideFilterConfirmationDialogMessage": {}, + "notEnoughSpaceDialogMessage": "Eragiketa hau burutzeko {neededSize}-eko leku librea behar da «{volume}»-(e)n, baina, {freeSize}-eko lekua dago soilik.", + "@notEnoughSpaceDialogMessage": { + "placeholders": { + "neededSize": { + "type": "String", + "example": "314 MB" + }, + "freeSize": { + "type": "String", + "example": "123 MB" + }, + "volume": { + "type": "String", + "example": "SD card", + "description": "the name of a storage volume" + } + } + }, + "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Album hau eta bertako elementua ezaba nahi al dituzu?} other{Album hau eta bertako {count} elementuak ezaba nahi al dituzu?}}", + "@deleteSingleAlbumConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "editEntryLocationDialogTitle": "Kokapena", + "@editEntryLocationDialogTitle": {}, + "editEntryLocationDialogChooseOnMap": "Aukeratu mapan", + "@editEntryLocationDialogChooseOnMap": {}, + "editEntryRatingDialogTitle": "Balorazioa", + "@editEntryRatingDialogTitle": {}, + "durationDialogHours": "Orduak", + "@durationDialogHours": {}, + "renameEntrySetPagePreviewSectionTitle": "Aurrebista", + "@renameEntrySetPagePreviewSectionTitle": {}, + "renameProcessorCounter": "Kontagailu", + "@renameProcessorCounter": {}, + "renameProcessorName": "Izena", + "@renameProcessorName": {}, + "editEntryLocationDialogSetCustom": "Ezarri kokapen pertsonalizatua", + "@editEntryLocationDialogSetCustom": {}, + "editEntryLocationDialogLatitude": "Latitudea", + "@editEntryLocationDialogLatitude": {}, + "editEntryDateDialogSourceFileModifiedDate": "Fitxategia aldatu zeneko data", + "@editEntryDateDialogSourceFileModifiedDate": {}, + "durationDialogMinutes": "Minutuak", + "@durationDialogMinutes": {}, + "durationDialogSeconds": "Segunduak", + "@durationDialogSeconds": {}, + "videoStreamSelectionDialogAudio": "Audioa", + "@videoStreamSelectionDialogAudio": {}, + "viewDialogSortSectionTitle": "Ordenatu", + "@viewDialogSortSectionTitle": {}, + "viewDialogGroupSectionTitle": "Taldea", + "@viewDialogGroupSectionTitle": {}, + "viewDialogLayoutSectionTitle": "Diseinua", + "@viewDialogLayoutSectionTitle": {}, + "tileLayoutMosaic": "Mosaikoa", + "@tileLayoutMosaic": {}, + "coverDialogTabCover": "Azala", + "@coverDialogTabCover": {}, + "aboutLinkPolicy": "Pribatutasun-politika", + "@aboutLinkPolicy": {}, + "videoStreamSelectionDialogTrack": "Pista", + "@videoStreamSelectionDialogTrack": {}, + "removeEntryMetadataDialogTitle": "Metadatuen ezabapena", + "@removeEntryMetadataDialogTitle": {}, + "videoStreamSelectionDialogOff": "Desgaitua", + "@videoStreamSelectionDialogOff": {}, + "videoStreamSelectionDialogVideo": "Bideoa", + "@videoStreamSelectionDialogVideo": {}, + "removeEntryMetadataDialogMore": "Gehiago", + "@removeEntryMetadataDialogMore": {}, + "genericSuccessFeedback": "Egina!", + "@genericSuccessFeedback": {}, + "genericFailureFeedback": "Huts egin du", + "@genericFailureFeedback": {}, + "genericDangerWarningDialogMessage": "Ziur al zaude?", + "@genericDangerWarningDialogMessage": {}, + "menuActionSelect": "Hautatu", + "@menuActionSelect": {}, + "menuActionSelectAll": "Guztiak hautatu", + "@menuActionSelectAll": {}, + "menuActionMap": "Mapa", + "@menuActionMap": {}, + "coverDialogTabApp": "Aplikazioa", + "@coverDialogTabApp": {}, + "appPickDialogTitle": "Aplikazioa aukeratu", + "@appPickDialogTitle": {}, + "appPickDialogNone": "Bat ere ez", + "@appPickDialogNone": {}, + "aboutPageTitle": "Honi buruz", + "@aboutPageTitle": {}, + "aboutLinkLicense": "Lizentzia", + "@aboutLinkLicense": {}, + "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "Mugimendu-argazkiko bideoa erreproduzitzeko XMP behar da.\n\nZiur al zaude ezabatu nahi duzula?", + "@removeEntryMetadataMotionPhotoXmpWarningDialogMessage": {}, + "videoSpeedDialogLabel": "Erreprodukzio-abiadura", + "@videoSpeedDialogLabel": {}, + "videoStreamSelectionDialogText": "Azpitituluak", + "@videoStreamSelectionDialogText": {}, + "videoStreamSelectionDialogNoSelection": "Ez dago pista gehiagorik.", + "@videoStreamSelectionDialogNoSelection": {}, + "tooManyItemsErrorDialogMessage": "Elementu gutxiagorekin saia zaitez berriro.", + "@tooManyItemsErrorDialogMessage": {}, + "menuActionConfigureView": "Ikusi", + "@menuActionConfigureView": {}, + "menuActionSlideshow": "Aurkezpena", + "@menuActionSlideshow": {}, + "menuActionSelectNone": "Bat ere ez hautatu", + "@menuActionSelectNone": {}, + "viewDialogReverseSortOrder": "Sailkapen-ordena alderantzikatu", + "@viewDialogReverseSortOrder": {}, + "menuActionStats": "Estatistikak", + "@menuActionStats": {}, + "tileLayoutGrid": "Sareta", + "@tileLayoutGrid": {}, + "tileLayoutList": "Zerrenda", + "@tileLayoutList": {}, + "coverDialogTabColor": "Kolorea", + "@coverDialogTabColor": {}, + "aboutBugSectionTitle": "Akatsen berri eman", + "@aboutBugSectionTitle": {}, + "aboutBugCopyInfoButton": "Kopiatu", + "@aboutBugCopyInfoButton": {}, + "aboutBugSaveLogInstruction": "Aplikazioaren erregistroak fitxategi batean gorde", + "@aboutBugSaveLogInstruction": {}, + "aboutBugCopyInfoInstruction": "Sistemako informazioa kopiatu", + "@aboutBugCopyInfoInstruction": {}, + "aboutTranslatorsSectionTitle": "Itzultzaileak", + "@aboutTranslatorsSectionTitle": {}, + "aboutLicensesBanner": "Aplikazio honek hurrengo kode-irekiko pakete eta liburutegiak erabiltzen ditu.", + "@aboutLicensesBanner": {}, + "aboutLicensesAndroidLibrariesSectionTitle": "Androideko liburutegiak", + "@aboutLicensesAndroidLibrariesSectionTitle": {}, + "collectionActionEdit": "Editatu", + "@collectionActionEdit": {}, + "aboutBugReportInstruction": "GitHubera txostena erregistro eta sistemako informazioarekin bidali", + "@aboutBugReportInstruction": {}, + "aboutCreditsSectionTitle": "Kredituak", + "@aboutCreditsSectionTitle": {}, + "aboutBugReportButton": "Txostena bidali", + "@aboutBugReportButton": {}, + "aboutCreditsWorldAtlas1": "Aplikazio honek TopoJSON fitxategi bat erabiltzen du", + "@aboutCreditsWorldAtlas1": {}, + "aboutLicensesFlutterPluginsSectionTitle": "Flutterreko pluginak", + "@aboutLicensesFlutterPluginsSectionTitle": {}, + "aboutCreditsWorldAtlas2": "ISC lizentziapekoa.", + "@aboutCreditsWorldAtlas2": {}, + "collectionSelectPageTitle": "Elementuak hautatu", + "@collectionSelectPageTitle": {}, + "collectionActionHideTitleSearch": "Izenburuen iragazkiak ezkutatu", + "@collectionActionHideTitleSearch": {}, + "collectionActionAddShortcut": "Lasterbidea gehitu", + "@collectionActionAddShortcut": {}, + "collectionActionCopy": "Albumera kopiatu", + "@collectionActionCopy": {}, + "aboutLicensesSectionTitle": "Kode-irekiko lizentziak", + "@aboutLicensesSectionTitle": {}, + "collectionActionMove": "Albumera mugitu", + "@collectionActionMove": {}, + "collectionActionRescan": "Berreskaneatu", + "@collectionActionRescan": {}, + "collectionActionShowTitleSearch": "Izenburuen iragazkiak erakutsi", + "@collectionActionShowTitleSearch": {}, + "collectionActionEmptyBin": "Zakarrontzia hustu", + "@collectionActionEmptyBin": {}, + "collectionGroupAlbum": "Albumaren arabera", + "@collectionGroupAlbum": {}, + "collectionGroupDay": "Egunaren arabera", + "@collectionGroupDay": {}, + "aboutLicensesFlutterPackagesSectionTitle": "Flutterreko paketeak", + "@aboutLicensesFlutterPackagesSectionTitle": {}, + "collectionPickPageTitle": "Aukeratu", + "@collectionPickPageTitle": {}, + "collectionSearchTitlesHintText": "Izenburuak bilatu", + "@collectionSearchTitlesHintText": {}, + "collectionGroupMonth": "Hilabetearen arabera", + "@collectionGroupMonth": {}, + "collectionGroupNone": "Ez taldekatu", + "@collectionGroupNone": {}, + "sectionUnknown": "Ezezaguna", + "@sectionUnknown": {}, + "dateThisMonth": "Hilabete honetan", + "@dateThisMonth": {}, + "collectionCopyFailureFeedback": "{count, plural, =1{Akatsa elementu 1 kopiatzean} other{Akatsa {count} elementu kopiatzean}}", + "@collectionCopyFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionMoveFailureFeedback": "{count, plural, =1{Akatsa elementu 1 mugitzean} other{Akatsa {count} elementu mugitzean}}", + "@collectionMoveFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionRenameFailureFeedback": "{count, plural, =1{Akatsa elementu 1 berrizendatzean} other{Akatsa {count} elementu berrizendatzean}}", + "@collectionRenameFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionEmptyFavourites": "Gogokorik ez", + "@collectionEmptyFavourites": {}, + "collectionCopySuccessFeedback": "{count, plural, =1{Elementu 1 kopiatuta} other{{count} elementu kopiatuta}}", + "@collectionCopySuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionMoveSuccessFeedback": "{count, plural, =1{Elementu 1 mugituta} other{{count} elementu mugituta}}", + "@collectionMoveSuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionRenameSuccessFeedback": "{count, plural, =1{Elementu 1 berrizendatuta} other{{count} elementu berrizendatuta}}", + "@collectionRenameSuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionEditSuccessFeedback": "{count, plural, =1{Elementu 1 editatuta} other{{count} elementu editatuta}}", + "@collectionEditSuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionEmptyImages": "Irudirik ez", + "@collectionEmptyImages": {}, + "collectionEmptyVideos": "Bideorik ez", + "@collectionEmptyVideos": {}, + "aboutLicensesDartPackagesSectionTitle": "Darteko paketeak", + "@aboutLicensesDartPackagesSectionTitle": {}, + "aboutLicensesShowAllButtonLabel": "Lizentzia guztiak erakutsi", + "@aboutLicensesShowAllButtonLabel": {}, + "policyPageTitle": "Pribatutasun-politika", + "@policyPageTitle": {}, + "collectionPageTitle": "Bilduma", + "@collectionPageTitle": {}, + "dateToday": "Gaur", + "@dateToday": {}, + "dateYesterday": "Atzo", + "@dateYesterday": {}, + "collectionDeleteFailureFeedback": "{count, plural, =1{Akatsa elementu 1 ezabatzean} other{Akatsa {count} elementu ezabatzean}}", + "@collectionDeleteFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionEditFailureFeedback": "{count, plural, =1{Akatsa elementu 1 editatzean} other{Akatsa {count} elementu editatzean}}", + "@collectionEditFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionExportFailureFeedback": "{count, plural, =1{Akatsa orri 1 esportatzean} other{Akatsa {count} orri esportatzean}}", + "@collectionExportFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "sortByRating": "Balorazioaren arabera", + "@sortByRating": {}, + "drawerSettingsButton": "Ezarpenak", + "@drawerSettingsButton": {}, + "drawerCountryPage": "Herrialdeak", + "@drawerCountryPage": {}, + "drawerCollectionPanoramas": "Panoramikak", + "@drawerCollectionPanoramas": {}, + "drawerAlbumPage": "Albumak", + "@drawerAlbumPage": {}, + "sortByItemCount": "Elementuen kopuruaren arabera", + "@sortByItemCount": {}, + "sortByAlbumFileName": "Album eta fitxategien izenaren arabera", + "@sortByAlbumFileName": {}, + "drawerCollectionAll": "Bilduma osoa", + "@drawerCollectionAll": {}, + "drawerCollectionFavourites": "Gogokoak", + "@drawerCollectionFavourites": {}, + "drawerCollectionImages": "Irudiak", + "@drawerCollectionImages": {}, + "drawerCollectionAnimated": "Animazioak", + "@drawerCollectionAnimated": {}, + "drawerCollectionMotionPhotos": "Mugimendu-argazkiak", + "@drawerCollectionMotionPhotos": {}, + "drawerCollectionRaws": "Raw argazkiak", + "@drawerCollectionRaws": {}, + "drawerCollectionSphericalVideos": "360°-ko bideoak", + "@drawerCollectionSphericalVideos": {}, + "drawerTagPage": "Etiketak", + "@drawerTagPage": {}, + "sortByDate": "Dataren arabera", + "@sortByDate": {}, + "sortOrderNewestFirst": "Berriena lehenengo", + "@sortOrderNewestFirst": {}, + "drawerCollectionVideos": "Bideoak", + "@drawerCollectionVideos": {}, + "sortByName": "Izenaren arabera", + "@sortByName": {}, + "sortBySize": "Tamainaren arabera", + "@sortBySize": {}, + "collectionEmptyGrantAccessButtonLabel": "Sarrera baimendu", + "@collectionEmptyGrantAccessButtonLabel": {}, + "collectionSelectSectionTooltip": "Atala aukeratu", + "@collectionSelectSectionTooltip": {}, + "collectionDeselectSectionTooltip": "Atala baztertu", + "@collectionDeselectSectionTooltip": {}, + "drawerAboutButton": "Honi buruz", + "@drawerAboutButton": {}, + "sortOrderAtoZ": "Atik Zra", + "@sortOrderAtoZ": {}, + "sortOrderZtoA": "Ztik Ara", + "@sortOrderZtoA": {}, + "sortOrderHighestFirst": "Altuena lehenengo", + "@sortOrderHighestFirst": {}, + "sortOrderLowestFirst": "Baxuena lehenengo", + "@sortOrderLowestFirst": {}, + "sortOrderLargestFirst": "Handiena lehenengo", + "@sortOrderLargestFirst": {}, + "sortOrderOldestFirst": "Zaharrena lehenengo", + "@sortOrderOldestFirst": {}, + "sortOrderSmallestFirst": "Txikiena lehenengo", + "@sortOrderSmallestFirst": {}, + "albumGroupTier": "Mailaren arabera", + "@albumGroupTier": {}, + "albumGroupType": "Motaren arabera", + "@albumGroupType": {}, + "albumGroupNone": "Ez taldekatu", + "@albumGroupNone": {}, + "albumPickPageTitleCopy": "Albumera kopiatu", + "@albumPickPageTitleCopy": {}, + "albumPickPageTitleExport": "Albumera esportatu", + "@albumPickPageTitleExport": {}, + "albumPickPageTitleMove": "Albumera mugitu", + "@albumPickPageTitleMove": {}, + "albumCamera": "Kamera", + "@albumCamera": {}, + "albumScreenRecordings": "Pantaila-grabaketak", + "@albumScreenRecordings": {}, + "albumGroupVolume": "Biltegiratze-tamainaren arabera", + "@albumGroupVolume": {}, + "albumDownload": "Deskargatu", + "@albumDownload": {}, + "albumMimeTypeMixed": "Nahastua", + "@albumMimeTypeMixed": {}, + "albumPickPageTitlePick": "Albuma aukeratu", + "@albumPickPageTitlePick": {}, + "albumScreenshots": "Pantaila-argazkiak", + "@albumScreenshots": {}, + "albumVideoCaptures": "Bideotako argazkiak", + "@albumVideoCaptures": {}, + "albumPageTitle": "Albumak", + "@albumPageTitle": {}, + "createAlbumButtonLabel": "SORTU", + "@createAlbumButtonLabel": {}, + "newFilterBanner": "berria", + "@newFilterBanner": {}, + "countryPageTitle": "Herrialdeak", + "@countryPageTitle": {}, + "tagPageTitle": "Etiketak", + "@tagPageTitle": {}, + "tagEmpty": "Etiketarik ez", + "@tagEmpty": {}, + "binPageTitle": "Zakarrontzia", + "@binPageTitle": {}, + "searchCollectionFieldHint": "Bilduman bilatu", + "@searchCollectionFieldHint": {}, + "searchRecentSectionTitle": "Berria", + "@searchRecentSectionTitle": {}, + "searchCountriesSectionTitle": "Herrialdeak", + "@searchCountriesSectionTitle": {}, + "searchPlacesSectionTitle": "Lekuak", + "@searchPlacesSectionTitle": {}, + "searchRatingSectionTitle": "Balorazioak", + "@searchRatingSectionTitle": {}, + "settingsSystemDefault": "Sistemakoa", + "@settingsSystemDefault": {}, + "settingsDisabled": "Desgaitua", + "@settingsDisabled": {}, + "settingsModificationWarningDialogMessage": "Beste ezarpenak aldatuko dira.", + "@settingsModificationWarningDialogMessage": {}, + "settingsSearchFieldLabel": "Bilaketaren ezarpenak", + "@settingsSearchFieldLabel": {}, + "settingsSearchEmpty": "Kointzidentziarik gabe", + "@settingsSearchEmpty": {}, + "settingsActionImport": "Inportatu", + "@settingsActionImport": {}, + "appExportFavourites": "Gogokoak", + "@appExportFavourites": {}, + "settingsNavigationSectionTitle": "Nabigazio", + "@settingsNavigationSectionTitle": {}, + "settingsHomeTile": "Hasiera", + "@settingsHomeTile": {}, + "settingsShowBottomNavigationBar": "Azpiko nabigazio-barra erakutsi", + "@settingsShowBottomNavigationBar": {}, + "settingsKeepScreenOnDialogTitle": "Pantaila piztuta mantendu", + "@settingsKeepScreenOnDialogTitle": {}, + "settingsDoubleBackExit": "Sakatu «atzera» bi aldiz irteteko", + "@settingsDoubleBackExit": {}, + "settingsConfirmationTile": "Baieztapen-leihoak", + "@settingsConfirmationTile": {}, + "settingsConfirmationBeforeDeleteItems": "Galdetu elementuak betirako ezabatu aurretik", + "@settingsConfirmationBeforeDeleteItems": {}, + "settingsConfirmationBeforeMoveToBinItems": "Galdetu elementuak zakarrontzira bidali aurretik", + "@settingsConfirmationBeforeMoveToBinItems": {}, + "settingsConfirmationBeforeMoveUndatedItems": "Galdetu data ez duten elementuak mugitu aurretik", + "@settingsConfirmationBeforeMoveUndatedItems": {}, + "settingsConfirmationAfterMoveToBinItems": "Mezua erakutsi elementuak zakarrontzira bidali ondoren", + "@settingsConfirmationAfterMoveToBinItems": {}, + "settingsNavigationDrawerEditorPageTitle": "Nabigazio-menua", + "@settingsNavigationDrawerEditorPageTitle": {}, + "settingsNavigationDrawerBanner": "Sakatu eta mantendu menuko elementuak mugitu edo berrordenatzeko.", + "@settingsNavigationDrawerBanner": {}, + "settingsNavigationDrawerTabTypes": "Motak", + "@settingsNavigationDrawerTabTypes": {}, + "settingsNavigationDrawerTabPages": "Orriak", + "@settingsNavigationDrawerTabPages": {}, + "settingsNavigationDrawerAddAlbum": "Albuma gehitu", + "@settingsNavigationDrawerAddAlbum": {}, + "settingsThumbnailSectionTitle": "Miniaturak", + "@settingsThumbnailSectionTitle": {}, + "settingsThumbnailShowFavouriteIcon": "Gogokoen ikonoa erakutsi", + "@settingsThumbnailShowFavouriteIcon": {}, + "settingsThumbnailShowTagIcon": "Etiketaren ikonoa erakutsi", + "@settingsThumbnailShowTagIcon": {}, + "settingsThumbnailShowMotionPhotoIcon": "Mugimendu-argazkiaren ikonoa erakutsi", + "@settingsThumbnailShowMotionPhotoIcon": {}, + "settingsThumbnailShowRawIcon": "Raw ikonoa erakutsi", + "@settingsThumbnailShowRawIcon": {}, + "settingsThumbnailShowVideoDuration": "Bideoaren iraupena erakutsi", + "@settingsThumbnailShowVideoDuration": {}, + "settingsCollectionQuickActionEditorPageTitle": "Ekintza azkarrak", + "@settingsCollectionQuickActionEditorPageTitle": {}, + "settingsCollectionQuickActionTabBrowsing": "Araketa", + "@settingsCollectionQuickActionTabBrowsing": {}, + "settingsThumbnailOverlayPageTitle": "Inkrustazioak", + "@settingsThumbnailOverlayPageTitle": {}, + "settingsThumbnailShowLocationIcon": "Kokapenaren ikonoa erakutsi", + "@settingsThumbnailShowLocationIcon": {}, + "settingsThumbnailShowRating": "Balorazioa erakutsi", + "@settingsThumbnailShowRating": {}, + "settingsCollectionQuickActionsTile": "Ekintza azkarrak", + "@settingsCollectionQuickActionsTile": {}, + "settingsCollectionQuickActionTabSelecting": "Aukeraketa", + "@settingsCollectionQuickActionTabSelecting": {}, + "searchMetadataSectionTitle": "Metadatuak", + "@searchMetadataSectionTitle": {}, + "settingsPageTitle": "Ezarpenak", + "@settingsPageTitle": {}, + "countryEmpty": "Herrialderik ez", + "@countryEmpty": {}, + "albumEmpty": "Albumik ez", + "@albumEmpty": {}, + "searchTagsSectionTitle": "Etiketak", + "@searchTagsSectionTitle": {}, + "searchDateSectionTitle": "Data", + "@searchDateSectionTitle": {}, + "settingsActionExport": "Esportatu", + "@settingsActionExport": {}, + "searchAlbumsSectionTitle": "Albumak", + "@searchAlbumsSectionTitle": {}, + "settingsDefault": "Lehenetsia", + "@settingsDefault": {}, + "settingsActionExportDialogTitle": "Esportazio", + "@settingsActionExportDialogTitle": {}, + "settingsKeepScreenOnTile": "Pantaila piztuta mantendu", + "@settingsKeepScreenOnTile": {}, + "settingsActionImportDialogTitle": "Inportazio", + "@settingsActionImportDialogTitle": {}, + "settingsHomeDialogTitle": "Hasiera", + "@settingsHomeDialogTitle": {}, + "appExportCovers": "Azalak", + "@appExportCovers": {}, + "appExportSettings": "Ezarpenak", + "@appExportSettings": {}, + "settingsNavigationDrawerTile": "Nabigazio-menua", + "@settingsNavigationDrawerTile": {}, + "settingsConfirmationDialogTitle": "Baieztapen-leihoak", + "@settingsConfirmationDialogTitle": {}, + "settingsNavigationDrawerTabAlbums": "Albumak", + "@settingsNavigationDrawerTabAlbums": {}, + "settingsThumbnailOverlayTile": "Inkrustazioak", + "@settingsThumbnailOverlayTile": {}, + "settingsViewerSectionTitle": "Bisorea", + "@settingsViewerSectionTitle": {}, + "settingsViewerGestureSideTapNext": "Pantailaren ertzetan sakatu aurreko/hurrengo elementua bistaratzeko", + "@settingsViewerGestureSideTapNext": {}, + "settingsViewerUseCutout": "Moztutako eremua erabili", + "@settingsViewerUseCutout": {}, + "settingsImageBackground": "Atzeko irudia", + "@settingsImageBackground": {}, + "settingsViewerQuickActionsTile": "Ekintza azkarrak", + "@settingsViewerQuickActionsTile": {}, + "settingsViewerQuickActionEditorPageTitle": "Ekintza azkarrak", + "@settingsViewerQuickActionEditorPageTitle": {}, + "settingsViewerQuickActionEditorBanner": "Sakatu eta luze mantendu botoiak mugitzeko eta bisorean zein ekintza bistaratuko diren aukeratzeko.", + "@settingsViewerQuickActionEditorBanner": {}, + "settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": "Bistaratutako botoiak", + "@settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": {}, + "settingsViewerQuickActionEditorAvailableButtonsSectionTitle": "Botoi erabilgarriak", + "@settingsViewerQuickActionEditorAvailableButtonsSectionTitle": {}, + "settingsViewerQuickActionEmpty": "Botoirik ez", + "@settingsViewerQuickActionEmpty": {}, + "settingsViewerOverlayPageTitle": "Inkrustazioak", + "@settingsViewerOverlayPageTitle": {}, + "settingsViewerShowMinimap": "Bistaratu minimapa", + "@settingsViewerShowMinimap": {}, + "settingsViewerShowInformation": "Bistaratu informazioa", + "@settingsViewerShowInformation": {}, + "settingsViewerShowInformationSubtitle": "Bistaratu izenburua, data, kokapena, etab.", + "@settingsViewerShowInformationSubtitle": {}, + "settingsViewerShowDescription": "Bistaratu deskribapena", + "@settingsViewerShowDescription": {}, + "settingsViewerShowOverlayThumbnails": "Bistaratu miniaturak", + "@settingsViewerShowOverlayThumbnails": {}, + "settingsSlideshowTransitionTile": "Trantsizioa", + "@settingsSlideshowTransitionTile": {}, + "settingsSlideshowIntervalTile": "Tartea", + "@settingsSlideshowIntervalTile": {}, + "settingsSlideshowVideoPlaybackDialogTitle": "Bideo-erreprodukzioa", + "@settingsSlideshowVideoPlaybackDialogTitle": {}, + "settingsVideoAutoPlay": "Autoerreprodukzioa", + "@settingsVideoAutoPlay": {}, + "settingsVideoLoopModeTile": "Begizta modua", + "@settingsVideoLoopModeTile": {}, + "settingsVideoLoopModeDialogTitle": "Begizta modua", + "@settingsVideoLoopModeDialogTitle": {}, + "settingsSubtitleThemeTile": "Azpitituluak", + "@settingsSubtitleThemeTile": {}, + "settingsSubtitleThemePageTitle": "Azpitituluak", + "@settingsSubtitleThemePageTitle": {}, + "settingsSubtitleThemeSample": "Hau adibide bat da.", + "@settingsSubtitleThemeSample": {}, + "settingsSubtitleThemeTextAlignmentDialogTitle": "Testuaren lerrokatzea", + "@settingsSubtitleThemeTextAlignmentDialogTitle": {}, + "settingsSubtitleThemeTextAlignmentTile": "Testuaren lerrokatzea", + "@settingsSubtitleThemeTextAlignmentTile": {}, + "settingsSubtitleThemeTextSize": "Testuaren tamaina", + "@settingsSubtitleThemeTextSize": {}, + "settingsSubtitleThemeShowOutline": "Bistaratu ingerada eta itzala", + "@settingsSubtitleThemeShowOutline": {}, + "settingsSubtitleThemeTextColor": "Testuaren kolorea", + "@settingsSubtitleThemeTextColor": {}, + "settingsSubtitleThemeTextOpacity": "Testuaren opakutasuna", + "@settingsSubtitleThemeTextOpacity": {}, + "settingsSubtitleThemeBackgroundColor": "Atzeko kolorea", + "@settingsSubtitleThemeBackgroundColor": {}, + "settingsSubtitleThemeBackgroundOpacity": "Atzeko opakutasuna", + "@settingsSubtitleThemeBackgroundOpacity": {}, + "settingsSubtitleThemeTextAlignmentLeft": "Ezkerra", + "@settingsSubtitleThemeTextAlignmentLeft": {}, + "settingsSubtitleThemeTextAlignmentCenter": "Erdigune", + "@settingsSubtitleThemeTextAlignmentCenter": {}, + "settingsSubtitleThemeTextAlignmentRight": "Eskuina", + "@settingsSubtitleThemeTextAlignmentRight": {}, + "settingsVideoGestureSideDoubleTapSeek": "Pantailaren ertzetan bi aldiz sakatu aurrera/atzera egiteko", + "@settingsVideoGestureSideDoubleTapSeek": {}, + "settingsVideoGestureVerticalDragBrightnessVolume": "Gora edo behera irristatu distira/bolumena egokitzeko", + "@settingsVideoGestureVerticalDragBrightnessVolume": {}, + "settingsPrivacySectionTitle": "Pribatutasuna", + "@settingsPrivacySectionTitle": {}, + "settingsAllowErrorReporting": "Onartu akatsen txosten anonimoa", + "@settingsAllowErrorReporting": {}, + "settingsSaveSearchHistory": "Gorde bilaketen historia", + "@settingsSaveSearchHistory": {}, + "settingsEnableBin": "Erabili zakarrontzia", + "@settingsEnableBin": {}, + "settingsAllowMediaManagement": "Baimendu mediaren kudeaketa", + "@settingsAllowMediaManagement": {}, + "settingsHiddenItemsTile": "Ezkutuko elementuak", + "@settingsHiddenItemsTile": {}, + "settingsHiddenItemsPageTitle": "Ezkutuko elementuak", + "@settingsHiddenItemsPageTitle": {}, + "settingsHiddenFiltersBanner": "Iragazkiekin bat datozen argazki eta bideoak ez dira zure bilduman agertuko.", + "@settingsHiddenFiltersBanner": {}, + "settingsHiddenItemsTabPaths": "Ezkutuko bideak", + "@settingsHiddenItemsTabPaths": {}, + "settingsHiddenFiltersEmpty": "Ezkutuko iragazkirik ez", + "@settingsHiddenFiltersEmpty": {}, + "settingsStorageAccessTile": "Biltegiratzerako sarrera", + "@settingsStorageAccessTile": {}, + "settingsStorageAccessBanner": "Karpeta batzuek baimen berezi bat behar dute bertak fitxategiak edita ahal izateko. Hemen, lehendik baimena eman diezun karpetak ikus ditzakezu.", + "@settingsStorageAccessBanner": {}, + "settingsStorageAccessRevokeTooltip": "Ezeztatu", + "@settingsStorageAccessRevokeTooltip": {}, + "settingsAccessibilitySectionTitle": "Irisgarritasuna", + "@settingsAccessibilitySectionTitle": {}, + "settingsRemoveAnimationsTile": "Kendu animazioak", + "@settingsRemoveAnimationsTile": {}, + "settingsRemoveAnimationsDialogTitle": "Kendu animazioak", + "@settingsRemoveAnimationsDialogTitle": {}, + "settingsTimeToTakeActionTile": "Ekintza aurretik igarotako denbora", + "@settingsTimeToTakeActionTile": {}, + "settingsCollectionSelectionQuickActionEditorBanner": "Sakatu eta luze mantendu botoiak mugitzeko eta elementuak aukeratzerakoan zein ekintza bistaratuko diren aukeratzeko.", + "@settingsCollectionSelectionQuickActionEditorBanner": {}, + "settingsCollectionBrowsingQuickActionEditorBanner": "Sakatu eta luze mantendu botoiak mugitzeko eta elementuak arakatzean zein ekintza bistaratuko diren aukeratzeko.", + "@settingsCollectionBrowsingQuickActionEditorBanner": {}, + "settingsViewerMaximumBrightness": "Gehienezko distira", + "@settingsViewerMaximumBrightness": {}, + "settingsMotionPhotoAutoPlay": "Automatikoki erreproduzitu mugimendu-argazkiak", + "@settingsMotionPhotoAutoPlay": {}, + "settingsViewerOverlayTile": "Inkrustazioak", + "@settingsViewerOverlayTile": {}, + "settingsViewerShowOverlayOnOpening": "Bistaratu hasieran", + "@settingsViewerShowOverlayOnOpening": {}, + "settingsViewerShowRatingTags": "Bistaratu balorazioa eta etiketak", + "@settingsViewerShowRatingTags": {}, + "settingsViewerEnableOverlayBlurEffect": "Lausotze efektua", + "@settingsViewerEnableOverlayBlurEffect": {}, + "settingsViewerShowShootingDetails": "Bistaratu hartualdiaren xehetasunak", + "@settingsViewerShowShootingDetails": {}, + "settingsViewerSlideshowPageTitle": "Aurkezpena", + "@settingsViewerSlideshowPageTitle": {}, + "settingsViewerSlideshowTile": "Aurkezpena", + "@settingsViewerSlideshowTile": {}, + "settingsSlideshowShuffle": "Nahastu", + "@settingsSlideshowShuffle": {}, + "settingsSlideshowFillScreen": "Pantaila bete", + "@settingsSlideshowFillScreen": {}, + "settingsSlideshowRepeat": "Errepikatu", + "@settingsSlideshowRepeat": {}, + "settingsSlideshowAnimatedZoomEffect": "Zoom animatuaren efektua", + "@settingsSlideshowAnimatedZoomEffect": {}, + "settingsSlideshowVideoPlaybackTile": "Bideo-erreprodukzioa", + "@settingsSlideshowVideoPlaybackTile": {}, + "settingsSubtitleThemeTextPositionTile": "Testuaren posizioa", + "@settingsSubtitleThemeTextPositionTile": {}, + "settingsVideoShowVideos": "Bideoak erakutsi", + "@settingsVideoShowVideos": {}, + "settingsVideoPageTitle": "Bideoen ezarpenak", + "@settingsVideoPageTitle": {}, + "settingsVideoSectionTitle": "Bideo", + "@settingsVideoSectionTitle": {}, + "settingsVideoEnableHardwareAcceleration": "Hardwarearen azelerazioa", + "@settingsVideoEnableHardwareAcceleration": {}, + "settingsVideoButtonsTile": "Botoiak", + "@settingsVideoButtonsTile": {}, + "settingsSubtitleThemeTextPositionDialogTitle": "Testuaren posizioa", + "@settingsSubtitleThemeTextPositionDialogTitle": {}, + "settingsVideoControlsTile": "Aginteak", + "@settingsVideoControlsTile": {}, + "settingsVideoGestureDoubleTapTogglePlay": "Bi aldiz sakatu erreproduzitzeko/gelditzeko", + "@settingsVideoGestureDoubleTapTogglePlay": {}, + "settingsVideoControlsPageTitle": "Aginteak", + "@settingsVideoControlsPageTitle": {}, + "settingsAllowInstalledAppAccess": "Aplikazioen zerrendarako sarrera baimendu", + "@settingsAllowInstalledAppAccess": {}, + "settingsAllowInstalledAppAccessSubtitle": "Albumen bistaratzea hobetzeko erabilia", + "@settingsAllowInstalledAppAccessSubtitle": {}, + "settingsEnableBinSubtitle": "Mantendu ezabatutako elementuak 30 egunez", + "@settingsEnableBinSubtitle": {}, + "settingsHiddenItemsTabFilters": "Ezkutuko iragazkiak", + "@settingsHiddenItemsTabFilters": {}, + "settingsHiddenPathsBanner": "Karpeta hauetako argazki eta bideoak, hauen azpikarpetak barne, ez dira zure bilduman agertuko.", + "@settingsHiddenPathsBanner": {}, + "addPathTooltip": "Gehitu bidea", + "@addPathTooltip": {}, + "settingsStorageAccessPageTitle": "Biltegiratzerako sarrera", + "@settingsStorageAccessPageTitle": {}, + "settingsStorageAccessEmpty": "Baimenik ez", + "@settingsStorageAccessEmpty": {}, + "settingsCollectionTile": "Bilduma", + "@settingsCollectionTile": {}, + "statsPageTitle": "Estatistikak", + "@statsPageTitle": {}, + "statsWithGps": "{count, plural, =1{Elementu 1 kokapenarekin} other{{count} elementu kokapenarekin}}", + "@statsWithGps": { + "placeholders": { + "count": {} + } + }, + "statsTopCountriesSectionTitle": "Herrialde nagusiak", + "@statsTopCountriesSectionTitle": {}, + "statsTopPlacesSectionTitle": "Leku nagusiak", + "@statsTopPlacesSectionTitle": {}, + "statsTopTagsSectionTitle": "Etiketa nagusiak", + "@statsTopTagsSectionTitle": {}, + "statsTopAlbumsSectionTitle": "Album nagusiak", + "@statsTopAlbumsSectionTitle": {}, + "viewerOpenPanoramaButtonLabel": "IREKI PANORAMIKA", + "@viewerOpenPanoramaButtonLabel": {}, + "viewerSetWallpaperButtonLabel": "EZARRI HORMA-PAPERA", + "@viewerSetWallpaperButtonLabel": {}, + "viewerErrorUnknown": "Ui!", + "@viewerErrorUnknown": {}, + "viewerErrorDoesNotExist": "Fitxategia jada ez da existitzen.", + "@viewerErrorDoesNotExist": {}, + "viewerInfoPageTitle": "Informazioa", + "@viewerInfoPageTitle": {}, + "viewerInfoBackToViewerTooltip": "Itzuli bisorera", + "@viewerInfoBackToViewerTooltip": {}, + "viewerInfoUnknown": "ezezaguna", + "@viewerInfoUnknown": {}, + "viewerInfoLabelDescription": "Deskribapena", + "@viewerInfoLabelDescription": {}, + "viewerInfoLabelTitle": "Izenburua", + "@viewerInfoLabelTitle": {}, + "viewerInfoLabelDate": "Data", + "@viewerInfoLabelDate": {}, + "viewerInfoLabelResolution": "Bereizmena", + "@viewerInfoLabelResolution": {}, + "viewerInfoLabelSize": "Tamaina", + "@viewerInfoLabelSize": {}, + "viewerInfoLabelUri": "URI", + "@viewerInfoLabelUri": {}, + "viewerInfoLabelPath": "Bidea", + "@viewerInfoLabelPath": {}, + "viewerInfoLabelDuration": "Iraupena", + "@viewerInfoLabelDuration": {}, + "viewerInfoLabelOwner": "Jabea", + "@viewerInfoLabelOwner": {}, + "viewerInfoLabelCoordinates": "Koordenatuak", + "@viewerInfoLabelCoordinates": {}, + "viewerInfoLabelAddress": "Helbidea", + "@viewerInfoLabelAddress": {}, + "mapStyleDialogTitle": "Maparen estiloa", + "@mapStyleDialogTitle": {}, + "mapStyleTooltip": "Hautatu maparen estiloa", + "@mapStyleTooltip": {}, + "mapZoomInTooltip": "Hurreratu", + "@mapZoomInTooltip": {}, + "mapZoomOutTooltip": "Aldendu", + "@mapZoomOutTooltip": {}, + "mapPointNorthUpTooltip": "Zuzendu iparra gorantz", + "@mapPointNorthUpTooltip": {}, + "openMapPageTooltip": "Ikusi maparen gunean", + "@openMapPageTooltip": {}, + "mapEmptyRegion": "Irudirik ez eskualde honetan", + "@mapEmptyRegion": {}, + "viewerInfoOpenEmbeddedFailureFeedback": "Huts egitea erantsitako data ateratzean", + "@viewerInfoOpenEmbeddedFailureFeedback": {}, + "viewerInfoOpenLinkText": "Ireki", + "@viewerInfoOpenLinkText": {}, + "viewerInfoSearchEmpty": "Bat datozen gakorik ez", + "@viewerInfoSearchEmpty": {}, + "viewerInfoSearchSuggestionDate": "Data eta ordua", + "@viewerInfoSearchSuggestionDate": {}, + "viewerInfoSearchSuggestionDescription": "Deskribapena", + "@viewerInfoSearchSuggestionDescription": {}, + "viewerInfoSearchSuggestionDimensions": "Dimentsioak", + "@viewerInfoSearchSuggestionDimensions": {}, + "viewerInfoSearchSuggestionResolution": "Bereizmena", + "@viewerInfoSearchSuggestionResolution": {}, + "viewerInfoSearchSuggestionRights": "Eskubideak", + "@viewerInfoSearchSuggestionRights": {}, + "tagEditorPageTitle": "Editatu etiketak", + "@tagEditorPageTitle": {}, + "tagPlaceholderCountry": "Herrialdea", + "@tagPlaceholderCountry": {}, + "tagEditorSectionPlaceholders": "Leku-markak", + "@tagEditorSectionPlaceholders": {}, + "tagPlaceholderPlace": "Lekua", + "@tagPlaceholderPlace": {}, + "panoramaEnableSensorControl": "Gaitu sentsoreen agintea", + "@panoramaEnableSensorControl": {}, + "panoramaDisableSensorControl": "Desgaitu sentsoreen agintea", + "@panoramaDisableSensorControl": {}, + "sourceViewerPageTitle": "Iturria", + "@sourceViewerPageTitle": {}, + "filePickerShowHiddenFiles": "Erakutsi ezkutuko fitxategiak", + "@filePickerShowHiddenFiles": {}, + "filePickerDoNotShowHiddenFiles": "Ez erakutsi ezkutuko fitxategiak", + "@filePickerDoNotShowHiddenFiles": {}, + "settingsThemeBrightnessTile": "Gaia", + "@settingsThemeBrightnessTile": {}, + "settingsDisplayUseTvInterface": "Android TV interfazea", + "@settingsDisplayUseTvInterface": {}, + "settingsThemeBrightnessDialogTitle": "Gaia", + "@settingsThemeBrightnessDialogTitle": {}, + "settingsAccessibilityShowPinchGestureAlternatives": "Ukimen ugariko keinu alternatiboak erakutsi", + "@settingsAccessibilityShowPinchGestureAlternatives": {}, + "settingsDisplaySectionTitle": "Pantaila", + "@settingsDisplaySectionTitle": {}, + "settingsThemeColorHighlights": "Nabarmen koloreak", + "@settingsThemeColorHighlights": {}, + "settingsThemeEnableDynamicColor": "Kolore dinamikoa", + "@settingsThemeEnableDynamicColor": {}, + "settingsDisplayRefreshRateModeTile": "Pantailaren freskatze-maiztasuna", + "@settingsDisplayRefreshRateModeTile": {}, + "settingsCoordinateFormatTile": "Koordenatuen formatua", + "@settingsCoordinateFormatTile": {}, + "settingsLanguageSectionTitle": "Hizkuntza eta formatuak", + "@settingsLanguageSectionTitle": {}, + "settingsLanguagePageTitle": "Hizkuntza", + "@settingsLanguagePageTitle": {}, + "settingsCoordinateFormatDialogTitle": "Koordenatuen formatua", + "@settingsCoordinateFormatDialogTitle": {}, + "settingsUnitSystemTile": "Unitateak", + "@settingsUnitSystemTile": {}, + "settingsScreenSaverPageTitle": "Pantaila-babeslea", + "@settingsScreenSaverPageTitle": {}, + "settingsDisplayRefreshRateModeDialogTitle": "Freskatze-maiztasuna", + "@settingsDisplayRefreshRateModeDialogTitle": {}, + "settingsLanguageTile": "Hizkuntza", + "@settingsLanguageTile": {}, + "settingsUnitSystemDialogTitle": "Unitateak", + "@settingsUnitSystemDialogTitle": {}, + "settingsWidgetPageTitle": "Argazki-markoa", + "@settingsWidgetPageTitle": {}, + "settingsWidgetShowOutline": "Ingerada", + "@settingsWidgetShowOutline": {}, + "settingsWidgetDisplayedItem": "Bistaratutako elementua", + "@settingsWidgetDisplayedItem": {}, + "tagEditorPageNewTagFieldLabel": "Etiketa berria", + "@tagEditorPageNewTagFieldLabel": {}, + "mapAttributionStamen": "Maparen datuak © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors • Teselak: [Stamen Design](https://stamen.com), [CC BY 3.0](https://creativecommons.org/licenses/by/3.0)", + "@mapAttributionStamen": {}, + "mapAttributionOsmHot": "Maparen datuak © [OpenStreetMap](https://www.openstreetmap.org/copyright) laguntzaileak • Teselak: [HOT](https://www.hotosm.org/) • Ostalaria: [OSM France](https://openstreetmap.fr/)", + "@mapAttributionOsmHot": {}, + "filePickerNoItems": "Elementurik ez", + "@filePickerNoItems": {}, + "viewerInfoSearchFieldLabel": "Bilatu metadatuak", + "@viewerInfoSearchFieldLabel": {}, + "viewerInfoViewXmlLinkText": "Ikusi XML", + "@viewerInfoViewXmlLinkText": {}, + "wallpaperUseScrollEffect": "Erabili lekualdatze-efektua hasierako pantailan", + "@wallpaperUseScrollEffect": {}, + "tagEditorPageAddTagTooltip": "Gehitu etiketa", + "@tagEditorPageAddTagTooltip": {}, + "tagEditorSectionRecent": "Berrienak", + "@tagEditorSectionRecent": {}, + "filePickerOpenFrom": "Ireki hemendik", + "@filePickerOpenFrom": {}, + "filePickerUseThisFolder": "Erabili karpeta hau", + "@filePickerUseThisFolder": {}, + "settingsWidgetOpenPage": "Widgetan sakatzean", + "@settingsWidgetOpenPage": {} +} diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 9c1c2e9a3..26d09535b 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -161,8 +161,8 @@ "@videoActionUnmute": {}, "videoActionSkip10": "جلو رفتن 10 ثانیه", "@videoActionSkip10": {}, - "videoActionSettings": "تنظیمات", - "@videoActionSettings": {}, + "viewerActionSettings": "تنظیمات", + "@viewerActionSettings": {}, "entryInfoActionEditRating": "ویرایش رتبه", "@entryInfoActionEditRating": {}, "entryInfoActionEditTags": "ویرایش برچسب ها", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index c62ac6bb8..5ea062a26 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -151,8 +151,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "Vitesse de lecture", "@videoActionSetSpeed": {}, - "videoActionSettings": "Préférences", - "@videoActionSettings": {}, + "viewerActionSettings": "Préférences", + "@viewerActionSettings": {}, "slideshowActionResume": "Reprendre", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Afficher dans Collection", @@ -703,8 +703,6 @@ "@albumPageTitle": {}, "albumEmpty": "Aucun album", "@albumEmpty": {}, - "createAlbumTooltip": "Créer un album", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "CRÉER", "@createAlbumButtonLabel": {}, "newFilterBanner": "nouveau", @@ -1210,5 +1208,45 @@ "tooManyItemsErrorDialogMessage": "Réessayez avec moins d’éléments.", "@tooManyItemsErrorDialogMessage": {}, "settingsVideoGestureVerticalDragBrightnessVolume": "Balayer verticalement pour ajuster la luminosité et le volume", - "@settingsVideoGestureVerticalDragBrightnessVolume": {} + "@settingsVideoGestureVerticalDragBrightnessVolume": {}, + "chipActionLock": "Verrouiller", + "@chipActionLock": {}, + "chipActionCreateVault": "Créer un coffre-fort", + "@chipActionCreateVault": {}, + "chipActionConfigureVault": "Configurer le coffre-fort", + "@chipActionConfigureVault": {}, + "albumTierVaults": "Coffres-forts", + "@albumTierVaults": {}, + "vaultLockTypePin": "Code PIN", + "@vaultLockTypePin": {}, + "vaultLockTypePassword": "Mot de passe", + "@vaultLockTypePassword": {}, + "newVaultWarningDialogMessage": "Les éléments dans les coffres-forts ne sont visibles que dans cette app et nulle autre.\n\nSi vous désinstallez cette app, ou que vous supprimez ses données, vous perdrez tous ces éléments.", + "@newVaultWarningDialogMessage": {}, + "newVaultDialogTitle": "Nouveau coffre-fort", + "@newVaultDialogTitle": {}, + "vaultDialogLockTypeLabel": "Verrouillage", + "@vaultDialogLockTypeLabel": {}, + "pinDialogEnter": "Entrez votre code PIN", + "@pinDialogEnter": {}, + "pinDialogConfirm": "Confirmez votre code PIN", + "@pinDialogConfirm": {}, + "passwordDialogConfirm": "Confirmez votre mot de passe", + "@passwordDialogConfirm": {}, + "authenticateToConfigureVault": "Authentification pour configurer le coffre-fort", + "@authenticateToConfigureVault": {}, + "vaultBinUsageDialogMessage": "Des coffres-forts utilisent la corbeille.", + "@vaultBinUsageDialogMessage": {}, + "vaultDialogLockModeWhenScreenOff": "Verrouiller quand l’écran s’éteint", + "@vaultDialogLockModeWhenScreenOff": {}, + "settingsConfirmationVaultDataLoss": "Afficher l’avertissement sur la perte de données avec les coffres-forts", + "@settingsConfirmationVaultDataLoss": {}, + "configureVaultDialogTitle": "Configuration du coffre-fort", + "@configureVaultDialogTitle": {}, + "passwordDialogEnter": "Entrez votre mot de passe", + "@passwordDialogEnter": {}, + "authenticateToUnlockVault": "Authentification pour déverrouiller le coffre-fort", + "@authenticateToUnlockVault": {}, + "settingsDisablingBinWarningDialogMessage": "Les éléments dans la corbeille seront supprimés définitivement.", + "@settingsDisablingBinWarningDialogMessage": {} } diff --git a/lib/l10n/app_gl.arb b/lib/l10n/app_gl.arb index 19cb3e1ed..6d135d232 100644 --- a/lib/l10n/app_gl.arb +++ b/lib/l10n/app_gl.arb @@ -119,8 +119,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "Velocidade de reprodución", "@videoActionSetSpeed": {}, - "videoActionSettings": "Configuración", - "@videoActionSettings": {}, + "viewerActionSettings": "Configuración", + "@viewerActionSettings": {}, "slideshowActionResume": "Resumo", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Mostrar na colección", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 1d9639b19..c688d386a 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -147,8 +147,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "Kecepatan pemutaran", "@videoActionSetSpeed": {}, - "videoActionSettings": "Pengaturan", - "@videoActionSettings": {}, + "viewerActionSettings": "Pengaturan", + "@viewerActionSettings": {}, "slideshowActionResume": "Lanjutkan", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Tampilkan di Koleksi", @@ -683,8 +683,6 @@ "@albumPageTitle": {}, "albumEmpty": "Tidak ada album", "@albumEmpty": {}, - "createAlbumTooltip": "Buat album", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "BUAT", "@createAlbumButtonLabel": {}, "newFilterBanner": "baru", @@ -1210,5 +1208,45 @@ "tooManyItemsErrorDialogMessage": "Coba lagi dengan item yang lebih sedikit.", "@tooManyItemsErrorDialogMessage": {}, "settingsVideoGestureVerticalDragBrightnessVolume": "Usap ke atas atau bawah untuk mengatur kecerahan/volume", - "@settingsVideoGestureVerticalDragBrightnessVolume": {} + "@settingsVideoGestureVerticalDragBrightnessVolume": {}, + "chipActionConfigureVault": "Atur brankas", + "@chipActionConfigureVault": {}, + "albumTierVaults": "Brankas", + "@albumTierVaults": {}, + "newVaultDialogTitle": "Brankas Baru", + "@newVaultDialogTitle": {}, + "configureVaultDialogTitle": "Atur Brankas", + "@configureVaultDialogTitle": {}, + "vaultDialogLockModeWhenScreenOff": "Kunci ketika layar mati", + "@vaultDialogLockModeWhenScreenOff": {}, + "vaultDialogLockTypeLabel": "Jenis penguncian", + "@vaultDialogLockTypeLabel": {}, + "pinDialogEnter": "Masukkan pin", + "@pinDialogEnter": {}, + "pinDialogConfirm": "Konfirmasi pin", + "@pinDialogConfirm": {}, + "passwordDialogEnter": "Masukkan kata sandi", + "@passwordDialogEnter": {}, + "passwordDialogConfirm": "Konfirmasi kata sandi", + "@passwordDialogConfirm": {}, + "authenticateToConfigureVault": "Autentikasi untuk mengatur brankas", + "@authenticateToConfigureVault": {}, + "authenticateToUnlockVault": "Autentikasi untuk membuka brankas", + "@authenticateToUnlockVault": {}, + "vaultBinUsageDialogMessage": "Beberapa brankas menggunakan tong sampah.", + "@vaultBinUsageDialogMessage": {}, + "settingsDisablingBinWarningDialogMessage": "Item dalam tong sampah akan hilang selamanya.", + "@settingsDisablingBinWarningDialogMessage": {}, + "chipActionLock": "Kunci", + "@chipActionLock": {}, + "vaultLockTypePassword": "Kata sandi", + "@vaultLockTypePassword": {}, + "chipActionCreateVault": "Buat brankas", + "@chipActionCreateVault": {}, + "vaultLockTypePin": "Pin", + "@vaultLockTypePin": {}, + "newVaultWarningDialogMessage": "Item dalam brankas hanya tersedia untuk aplikasi ini dan bukan yang lain.\n\nJika Anda menghapus aplikasi ini, atau menghapus data aplikasi ini, Anda akan kehilangan semua item tersebut.", + "@newVaultWarningDialogMessage": {}, + "settingsConfirmationVaultDataLoss": "Tampilkan peringatan kehilangan data brankas", + "@settingsConfirmationVaultDataLoss": {} } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index a9577f7bc..9f533475d 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -151,8 +151,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "Velocità di riproduzione", "@videoActionSetSpeed": {}, - "videoActionSettings": "Impostazioni", - "@videoActionSettings": {}, + "viewerActionSettings": "Impostazioni", + "@viewerActionSettings": {}, "slideshowActionResume": "Riprendi", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Mostra nella Collezione", @@ -699,8 +699,6 @@ "@albumPageTitle": {}, "albumEmpty": "Nessun album", "@albumEmpty": {}, - "createAlbumTooltip": "Crea album", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "CREA", "@createAlbumButtonLabel": {}, "newFilterBanner": "nuovo", @@ -1208,5 +1206,7 @@ "settingsViewerShowDescription": "Mostra la descrizione", "@settingsViewerShowDescription": {}, "tooManyItemsErrorDialogMessage": "Riprova con meno elementi.", - "@tooManyItemsErrorDialogMessage": {} + "@tooManyItemsErrorDialogMessage": {}, + "settingsVideoGestureVerticalDragBrightnessVolume": "Trascina su o giù per aggiustare luminosità/volume", + "@settingsVideoGestureVerticalDragBrightnessVolume": {} } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 3c0255923..d909c5baf 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -147,8 +147,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "再生速度", "@videoActionSetSpeed": {}, - "videoActionSettings": "設定", - "@videoActionSettings": {}, + "viewerActionSettings": "設定", + "@viewerActionSettings": {}, "slideshowActionResume": "再開", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "コレクションで表示", @@ -657,8 +657,6 @@ "@albumPageTitle": {}, "albumEmpty": "アルバムはありません", "@albumEmpty": {}, - "createAlbumTooltip": "アルバムを作成", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "作成", "@createAlbumButtonLabel": {}, "newFilterBanner": "新規", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 4a3c0ee2a..d3f3061d3 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -151,8 +151,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "재생 배속", "@videoActionSetSpeed": {}, - "videoActionSettings": "설정", - "@videoActionSettings": {}, + "viewerActionSettings": "설정", + "@viewerActionSettings": {}, "slideshowActionResume": "이어서", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "미디어 페이지에서 보기", @@ -703,8 +703,6 @@ "@albumPageTitle": {}, "albumEmpty": "앨범이 없습니다", "@albumEmpty": {}, - "createAlbumTooltip": "앨범 만들기", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "추가", "@createAlbumButtonLabel": {}, "newFilterBanner": "신규", @@ -1210,5 +1208,45 @@ "tooManyItemsErrorDialogMessage": "항목 수를 줄이고 다시 시도하세요.", "@tooManyItemsErrorDialogMessage": {}, "settingsVideoGestureVerticalDragBrightnessVolume": "위아래로 스와이프해서 밝기/음량을 조절하기", - "@settingsVideoGestureVerticalDragBrightnessVolume": {} + "@settingsVideoGestureVerticalDragBrightnessVolume": {}, + "albumTierVaults": "금고", + "@albumTierVaults": {}, + "chipActionLock": "잠금", + "@chipActionLock": {}, + "vaultLockTypePin": "PIN", + "@vaultLockTypePin": {}, + "vaultLockTypePassword": "비밀번호", + "@vaultLockTypePassword": {}, + "configureVaultDialogTitle": "금고 설정", + "@configureVaultDialogTitle": {}, + "pinDialogEnter": "PIN을 입력하세요", + "@pinDialogEnter": {}, + "passwordDialogEnter": "비밀번호를 입력하세요", + "@passwordDialogEnter": {}, + "pinDialogConfirm": "PIN을 확인하세요", + "@pinDialogConfirm": {}, + "vaultDialogLockTypeLabel": "잠금 방식", + "@vaultDialogLockTypeLabel": {}, + "vaultDialogLockModeWhenScreenOff": "화면이 꺼진 후 자동으로 잠김", + "@vaultDialogLockModeWhenScreenOff": {}, + "authenticateToConfigureVault": "금고 설정을 위한 인증", + "@authenticateToConfigureVault": {}, + "authenticateToUnlockVault": "금고 잠금 해제를 위한 인증", + "@authenticateToUnlockVault": {}, + "settingsDisablingBinWarningDialogMessage": "휴지통에 있는 항목들이 완전히 삭제될 것입니다.", + "@settingsDisablingBinWarningDialogMessage": {}, + "newVaultWarningDialogMessage": "금고에 있는 항목들은 이 앱에서만 볼 수 있습니다.\n\n이 앱을 삭제 시, 또한 이 앱의 데이터를 삭제 시, 항목을 완전히 삭제될 것입니다.", + "@newVaultWarningDialogMessage": {}, + "chipActionCreateVault": "금고 만들기", + "@chipActionCreateVault": {}, + "chipActionConfigureVault": "금고 설정", + "@chipActionConfigureVault": {}, + "newVaultDialogTitle": "새 금고 만들기", + "@newVaultDialogTitle": {}, + "passwordDialogConfirm": "비밀번호를 확인하세요", + "@passwordDialogConfirm": {}, + "vaultBinUsageDialogMessage": "휴지통을 사용하는 금고가 있습니다.", + "@vaultBinUsageDialogMessage": {}, + "settingsConfirmationVaultDataLoss": "금고에 관한 데이터 손실 경고", + "@settingsConfirmationVaultDataLoss": {} } diff --git a/lib/l10n/app_lt.arb b/lib/l10n/app_lt.arb index 753348ed7..fb9391907 100644 --- a/lib/l10n/app_lt.arb +++ b/lib/l10n/app_lt.arb @@ -97,8 +97,8 @@ "@videoActionSkip10": {}, "videoActionSetSpeed": "Atkūrimo greitis", "@videoActionSetSpeed": {}, - "videoActionSettings": "Nustatymai", - "@videoActionSettings": {}, + "viewerActionSettings": "Nustatymai", + "@viewerActionSettings": {}, "slideshowActionResume": "Tęsti", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Rodyti kolekcijoje", @@ -1171,8 +1171,6 @@ "@albumPageTitle": {}, "albumEmpty": "Nėra albumų", "@albumEmpty": {}, - "createAlbumTooltip": "Sukurti albumą", - "@createAlbumTooltip": {}, "newFilterBanner": "nauja", "@newFilterBanner": {}, "binPageTitle": "Šiukšlinė", diff --git a/lib/l10n/app_nb.arb b/lib/l10n/app_nb.arb index 0c98e35ba..69cb53c56 100644 --- a/lib/l10n/app_nb.arb +++ b/lib/l10n/app_nb.arb @@ -76,8 +76,8 @@ "@videoActionSkip10": {}, "videoActionSetSpeed": "Avspillingshastighet", "@videoActionSetSpeed": {}, - "videoActionSettings": "Innstillinger", - "@videoActionSettings": {}, + "viewerActionSettings": "Innstillinger", + "@viewerActionSettings": {}, "entryInfoActionEditTitleDescription": "Rediger navn og beskrivelse", "@entryInfoActionEditTitleDescription": {}, "filterNoDateLabel": "Udatert", @@ -709,8 +709,6 @@ "@albumVideoCaptures": {}, "albumEmpty": "Ingen album", "@albumEmpty": {}, - "createAlbumTooltip": "Opprett album", - "@createAlbumTooltip": {}, "binPageTitle": "Papirkurv", "@binPageTitle": {}, "countryPageTitle": "Land", @@ -1366,5 +1364,7 @@ "settingsModificationWarningDialogMessage": "Andre innstillinger vil bli endret.", "@settingsModificationWarningDialogMessage": {}, "tooManyItemsErrorDialogMessage": "Prøv igjen med færre elementer.", - "@tooManyItemsErrorDialogMessage": {} + "@tooManyItemsErrorDialogMessage": {}, + "settingsVideoGestureVerticalDragBrightnessVolume": "Dra opp eller ned for å justere lys-/lydstyrke", + "@settingsVideoGestureVerticalDragBrightnessVolume": {} } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index d1cc8730f..017cba23d 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -151,8 +151,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "Afspeelsnelheid", "@videoActionSetSpeed": {}, - "videoActionSettings": "Instellingen", - "@videoActionSettings": {}, + "viewerActionSettings": "Instellingen", + "@viewerActionSettings": {}, "slideshowActionResume": "Hervatten", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Tonen in Collectie", @@ -693,8 +693,6 @@ "@albumPageTitle": {}, "albumEmpty": "Geen albums", "@albumEmpty": {}, - "createAlbumTooltip": "Album aanmaken", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "AANMAKEN", "@createAlbumButtonLabel": {}, "newFilterBanner": "nieuw", diff --git a/lib/l10n/app_nn.arb b/lib/l10n/app_nn.arb index d4bd4a9a3..11c247896 100644 --- a/lib/l10n/app_nn.arb +++ b/lib/l10n/app_nn.arb @@ -170,8 +170,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "Avspelingssnøggleik", "@videoActionSetSpeed": {}, - "videoActionSettings": "Innstillingar", - "@videoActionSettings": {}, + "viewerActionSettings": "Innstillingar", + "@viewerActionSettings": {}, "slideshowActionResume": "Hald fram", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Vis i Samling", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 2ef13f337..df6bb0c05 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -85,8 +85,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "Prędkość odtwarzania", "@videoActionSetSpeed": {}, - "videoActionSettings": "Ustawienia", - "@videoActionSettings": {}, + "viewerActionSettings": "Ustawienia", + "@viewerActionSettings": {}, "slideshowActionResume": "Wznów", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Pokaż w Kolekcji", @@ -235,31 +235,31 @@ "@displayRefreshRatePreferLowest": {}, "videoPlaybackMuted": "Odtwarzaj bez dźwięku", "@videoPlaybackMuted": {}, - "itemCount": "{count, plural, =1{1 element} =2..4{{count} elementy} other{{count} elelmentów}}", + "itemCount": "{count, plural, =1{1 element} few{{count} elementy} other{{count} elelmentów}}", "@itemCount": { "placeholders": { "count": {} } }, - "columnCount": "{count, plural, =1{1 rząd} =2..4{{count} rzędy} other{{count} rzędów}}", + "columnCount": "{count, plural, =1{1 rząd} few{{count} rzędy} other{{count} rzędów}}", "@columnCount": { "placeholders": { "count": {} } }, - "timeSeconds": "{seconds, plural, =1{1 sekunda} =2..4{{seconds} sekundy} other{{seconds} sekund}}", + "timeSeconds": "{seconds, plural, =1{1 sekunda} few{{seconds} sekundy} other{{seconds} sekund}}", "@timeSeconds": { "placeholders": { "seconds": {} } }, - "timeMinutes": "{minutes, plural, =1{1 minuta} =2..4{{minutes} minuty} other{{minutes} minut}}", + "timeMinutes": "{minutes, plural, =1{1 minuta} few{{minutes} minuty} other{{minutes} minut}}", "@timeMinutes": { "placeholders": { "minutes": {} } }, - "timeDays": "{days, plural, =1{1 dzień} =2..4{{days} dni} other{{days} dni}}", + "timeDays": "{days, plural, =1{1 dzień} few{{days} dni} other{{days} dni}}", "@timeDays": { "placeholders": { "days": {} @@ -547,7 +547,7 @@ "@renameProcessorCounter": {}, "renameProcessorName": "Nazwa", "@renameProcessorName": {}, - "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Usunąć ten album i jego element?} =2..4{Usunąć ten album i jego {count} elementy?} other{Usunąć ten album i jego {count} elementów?}}", + "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Usunąć ten album i jego element?} few{Usunąć ten album i jego {count} elementy?} other{Usunąć ten album i jego {count} elementów?}}", "@deleteSingleAlbumConfirmationDialogMessage": { "placeholders": { "count": {} @@ -603,13 +603,13 @@ "@sectionUnknown": {}, "dateToday": "Dzisiaj", "@dateToday": {}, - "collectionCopySuccessFeedback": "{count, plural, =1{Skopiowano 1 element} =2..4{Skopiowano {count} elementy} other{Skopiowano {count} elementów}}", + "collectionCopySuccessFeedback": "{count, plural, =1{Skopiowano 1 element} few{Skopiowano {count} elementy} other{Skopiowano {count} elementów}}", "@collectionCopySuccessFeedback": { "placeholders": { "count": {} } }, - "collectionEditSuccessFeedback": "{count, plural, =1{Wyedytowano 1 element} =2..4{Wyedytowano {count} elementy} other{Wyedytowano {count} elementów}}", + "collectionEditSuccessFeedback": "{count, plural, =1{Wyedytowano 1 element} few{Wyedytowano {count} elementy} other{Wyedytowano {count} elementów}}", "@collectionEditSuccessFeedback": { "placeholders": { "count": {} @@ -705,8 +705,6 @@ "@sortByItemCount": {}, "sortBySize": "Według rozmiaru", "@sortBySize": {}, - "createAlbumTooltip": "Utwórz album", - "@createAlbumTooltip": {}, "albumEmpty": "Brak albumów", "@albumEmpty": {}, "renameEntrySetPageTitle": "Zmień nazwę", @@ -815,7 +813,7 @@ "@albumGroupType": {}, "renameEntrySetPagePreviewSectionTitle": "Podgląd", "@renameEntrySetPagePreviewSectionTitle": {}, - "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Usunąć te albumy i ich element?} =2..4{Usunąć te albumy i ich {count} elementy?} other{Usunąć te albumy i ich {count} elementów?}}", + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Usunąć te albumy i ich element?} few{Usunąć te albumy i ich {count} elementy?} other{Usunąć te albumy i ich {count} elementów?}}", "@deleteMultiAlbumConfirmationDialogMessage": { "placeholders": { "count": {} @@ -903,7 +901,7 @@ "count": {} } }, - "collectionMoveSuccessFeedback": "{count, plural, =1{Przeniesiono 1 element} =2..4{Przeniesiono {count} elementy} other{Przeniesiono {count} elementów}}", + "collectionMoveSuccessFeedback": "{count, plural, =1{Przeniesiono 1 element} few{Przeniesiono {count} elementy} other{Przeniesiono {count} elementów}}", "@collectionMoveSuccessFeedback": { "placeholders": { "count": {} @@ -1319,7 +1317,7 @@ "@settingsStorageAccessPageTitle": {}, "settingsStorageAccessBanner": "Niektóre katalogi wymagają jawnego udzielenia dostępu, aby modyfikować znajdujące się w nich pliki. Możesz przejrzeć tutaj katalogi, do których wcześniej udzielono dostępu.", "@settingsStorageAccessBanner": {}, - "statsWithGps": "{count, plural, =1{1 element z położeniem} =2..4{{count} elementy z położeniem} other{{count} elementów z położeniem}}", + "statsWithGps": "{count, plural, =1{1 element z położeniem} few{{count} elementy z położeniem} other{{count} elementów z położeniem}}", "@statsWithGps": { "placeholders": { "count": {} @@ -1368,5 +1366,45 @@ "tooManyItemsErrorDialogMessage": "Spróbuj ponownie z mniejszą ilością elementów.", "@tooManyItemsErrorDialogMessage": {}, "settingsVideoGestureVerticalDragBrightnessVolume": "Przesuń palcem w górę lub w dół, aby dostosować jasność/głośność", - "@settingsVideoGestureVerticalDragBrightnessVolume": {} + "@settingsVideoGestureVerticalDragBrightnessVolume": {}, + "chipActionLock": "Zablokuj", + "@chipActionLock": {}, + "albumTierVaults": "Skarbce", + "@albumTierVaults": {}, + "chipActionConfigureVault": "Konfiguruj Skarbiec", + "@chipActionConfigureVault": {}, + "vaultLockTypePassword": "Hasło", + "@vaultLockTypePassword": {}, + "newVaultDialogTitle": "Nowy Skarbiec", + "@newVaultDialogTitle": {}, + "configureVaultDialogTitle": "Konfiguruj Skarbiec", + "@configureVaultDialogTitle": {}, + "vaultDialogLockModeWhenScreenOff": "Blokada po wyłączeniu ekranu", + "@vaultDialogLockModeWhenScreenOff": {}, + "vaultDialogLockTypeLabel": "Rodzaj blokady", + "@vaultDialogLockTypeLabel": {}, + "pinDialogConfirm": "Potwierdź kod PIN", + "@pinDialogConfirm": {}, + "passwordDialogEnter": "Wpisz hasło", + "@passwordDialogEnter": {}, + "pinDialogEnter": "Wpisz kod PIN", + "@pinDialogEnter": {}, + "passwordDialogConfirm": "Potwierdź hasło", + "@passwordDialogConfirm": {}, + "authenticateToUnlockVault": "Uwierzytelnij się, aby odblokować skarbiec", + "@authenticateToUnlockVault": {}, + "authenticateToConfigureVault": "Uwierzytelnij się, aby skonfigurować Skarbiec", + "@authenticateToConfigureVault": {}, + "settingsConfirmationVaultDataLoss": "Pokaż ostrzeżenie o utracie danych skarbca", + "@settingsConfirmationVaultDataLoss": {}, + "settingsDisablingBinWarningDialogMessage": "Elementy znajdujące się w koszu zostaną usunięte na zawsze.", + "@settingsDisablingBinWarningDialogMessage": {}, + "chipActionCreateVault": "Utwórz skarbiec", + "@chipActionCreateVault": {}, + "newVaultWarningDialogMessage": "Elementy w skarbcach są dostępne tylko dla tej aplikacji i żadnej innej.\n\nJeśli odinstalujesz tę aplikację lub wyczyścisz dane tej aplikacji, stracisz wszystkie te elementy.", + "@newVaultWarningDialogMessage": {}, + "vaultBinUsageDialogMessage": "Niektóre skarbce korzystają z kosza.", + "@vaultBinUsageDialogMessage": {}, + "vaultLockTypePin": "PIN", + "@vaultLockTypePin": {} } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 075bb96f4..c4a8769ad 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -151,8 +151,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "Velocidade de reprodução", "@videoActionSetSpeed": {}, - "videoActionSettings": "Configurações", - "@videoActionSettings": {}, + "viewerActionSettings": "Configurações", + "@viewerActionSettings": {}, "slideshowActionResume": "Retomar", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Mostrar na Coleção", @@ -699,8 +699,6 @@ "@albumPageTitle": {}, "albumEmpty": "Nenhum álbum", "@albumEmpty": {}, - "createAlbumTooltip": "Criar álbum", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "CRIA", "@createAlbumButtonLabel": {}, "newFilterBanner": "novo", diff --git a/lib/l10n/app_ro.arb b/lib/l10n/app_ro.arb index 4fe017638..38330dc18 100644 --- a/lib/l10n/app_ro.arb +++ b/lib/l10n/app_ro.arb @@ -152,8 +152,8 @@ "@videoActionSkip10": {}, "videoActionSelectStreams": "Selectați piese", "@videoActionSelectStreams": {}, - "videoActionSettings": "Setări", - "@videoActionSettings": {}, + "viewerActionSettings": "Setări", + "@viewerActionSettings": {}, "slideshowActionResume": "Reluare", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Afișați în colecție", @@ -743,8 +743,6 @@ "@sortByRating": {}, "viewerInfoLabelUri": "URI", "@viewerInfoLabelUri": {}, - "createAlbumTooltip": "Creați un album", - "@createAlbumTooltip": {}, "viewerInfoLabelDescription": "Descriere", "@viewerInfoLabelDescription": {}, "settingsThumbnailOverlayPageTitle": "Suprapunere", @@ -1366,5 +1364,47 @@ "settingsDisplayUseTvInterface": "Interfață Android TV", "@settingsDisplayUseTvInterface": {}, "tooManyItemsErrorDialogMessage": "Încearcă din nou cu mai puține elemente.", - "@tooManyItemsErrorDialogMessage": {} + "@tooManyItemsErrorDialogMessage": {}, + "settingsVideoGestureVerticalDragBrightnessVolume": "Glisați în sus sau în jos pentru a regla luminozitatea/volumul", + "@settingsVideoGestureVerticalDragBrightnessVolume": {}, + "chipActionLock": "Blocare", + "@chipActionLock": {}, + "chipActionCreateVault": "Creare seif", + "@chipActionCreateVault": {}, + "chipActionConfigureVault": "Configurare seif", + "@chipActionConfigureVault": {}, + "albumTierVaults": "Seifuri", + "@albumTierVaults": {}, + "newVaultDialogTitle": "Seif nou", + "@newVaultDialogTitle": {}, + "configureVaultDialogTitle": "Configurare seif", + "@configureVaultDialogTitle": {}, + "vaultDialogLockModeWhenScreenOff": "Blocare atunci când ecranul se oprește", + "@vaultDialogLockModeWhenScreenOff": {}, + "vaultDialogLockTypeLabel": "Tip de blocare", + "@vaultDialogLockTypeLabel": {}, + "pinDialogConfirm": "Confirmă codul PIN", + "@pinDialogConfirm": {}, + "pinDialogEnter": "Introdu codul PIN", + "@pinDialogEnter": {}, + "passwordDialogEnter": "Introdu parola", + "@passwordDialogEnter": {}, + "passwordDialogConfirm": "Confirmă parola", + "@passwordDialogConfirm": {}, + "authenticateToConfigureVault": "Autentifică-te pentru a configura seiful", + "@authenticateToConfigureVault": {}, + "authenticateToUnlockVault": "Autentifică-te pentru a debloca seiful", + "@authenticateToUnlockVault": {}, + "vaultBinUsageDialogMessage": "Unele seifuri folosesc coșul de reciclare.", + "@vaultBinUsageDialogMessage": {}, + "settingsDisablingBinWarningDialogMessage": "Articolele din coșul de reciclare vor fi șterse pentru totdeauna.", + "@settingsDisablingBinWarningDialogMessage": {}, + "vaultLockTypePin": "Pin", + "@vaultLockTypePin": {}, + "vaultLockTypePassword": "Parolă", + "@vaultLockTypePassword": {}, + "newVaultWarningDialogMessage": "Elementele din seifuri sunt disponibile doar pentru această aplicație și nu pentru altele.\n\nDacă dezinstalezi această aplicație sau ștergi datele acestei aplicații, vei pierde toate aceste elemente.", + "@newVaultWarningDialogMessage": {}, + "settingsConfirmationVaultDataLoss": "Afișare avertisment privind pierderile de date din seif", + "@settingsConfirmationVaultDataLoss": {} } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 3cd48cd05..9732c9f56 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -151,8 +151,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "Скорость вопспроизведения", "@videoActionSetSpeed": {}, - "videoActionSettings": "Настройки", - "@videoActionSettings": {}, + "viewerActionSettings": "Настройки", + "@viewerActionSettings": {}, "slideshowActionResume": "Продолжить", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Показать в Коллекции", @@ -699,8 +699,6 @@ "@albumPageTitle": {}, "albumEmpty": "Нет альбомов", "@albumEmpty": {}, - "createAlbumTooltip": "Создать альбом", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "СОЗДАТЬ", "@createAlbumButtonLabel": {}, "newFilterBanner": "новый", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb new file mode 100644 index 000000000..07321663d --- /dev/null +++ b/lib/l10n/app_sk.arb @@ -0,0 +1,456 @@ +{ + "deleteButtonLabel": "ODSTRÁNIŤ", + "@deleteButtonLabel": {}, + "appName": "Aves", + "@appName": {}, + "welcomeMessage": "Vitajte v Aves", + "@welcomeMessage": {}, + "cancelTooltip": "ZRUŠIŤ", + "@cancelTooltip": {}, + "changeTooltip": "ZMENIŤ", + "@changeTooltip": {}, + "clearTooltip": "VYČISTIŤ", + "@clearTooltip": {}, + "previousTooltip": "PREDCHÁDZAJÚCE", + "@previousTooltip": {}, + "nextTooltip": "Ďalej", + "@nextTooltip": {}, + "showTooltip": "Zobraziť", + "@showTooltip": {}, + "resetTooltip": "Znovu", + "@resetTooltip": {}, + "saveTooltip": "Uložiť", + "@saveTooltip": {}, + "pickTooltip": "Vybrať", + "@pickTooltip": {}, + "chipActionDelete": "Odstrániť", + "@chipActionDelete": {}, + "welcomeTermsToggle": "Súhlasím s pravidlami a podmienkami", + "@welcomeTermsToggle": {}, + "timeDays": "{days, plural, other{{days} dni}}", + "@timeDays": { + "placeholders": { + "days": {} + } + }, + "focalLength": "{length} mm", + "@focalLength": { + "placeholders": { + "length": { + "type": "String", + "example": "5.4" + } + } + }, + "applyButtonLabel": "POUŽIŤ", + "@applyButtonLabel": {}, + "continueButtonLabel": "POKRAČOVAŤ", + "@continueButtonLabel": {}, + "doubleBackExitMessage": "Stlač znovu \"späť\" pre ukončenie.", + "@doubleBackExitMessage": {}, + "welcomeOptional": "Voliteľné", + "@welcomeOptional": {}, + "timeMinutes": "{minutes, plural, other{{minutes} minúty}}", + "@timeMinutes": { + "placeholders": { + "minutes": {} + } + }, + "nextButtonLabel": "ĎALEJ", + "@nextButtonLabel": {}, + "showButtonLabel": "ZOBRAZIŤ", + "@showButtonLabel": {}, + "hideButtonLabel": "SCHOVAŤ", + "@hideButtonLabel": {}, + "hideTooltip": "Schovať", + "@hideTooltip": {}, + "actionRemove": "Odstrániť", + "@actionRemove": {}, + "sourceStateLoading": "Načítavanie", + "@sourceStateLoading": {}, + "sourceStateCataloguing": "Indexovanie", + "@sourceStateCataloguing": {}, + "doNotAskAgain": "Nepýtať sa znovu", + "@doNotAskAgain": {}, + "chipActionGoToAlbumPage": "Zobraziť v albumoch", + "@chipActionGoToAlbumPage": {}, + "sourceStateLocatingCountries": "Hľadanie krajín", + "@sourceStateLocatingCountries": {}, + "sourceStateLocatingPlaces": "Hľadanie miest", + "@sourceStateLocatingPlaces": {}, + "chipActionGoToCountryPage": "Zobraziť v krajinách", + "@chipActionGoToCountryPage": {}, + "chipActionGoToTagPage": "Zobraziť v označeniach", + "@chipActionGoToTagPage": {}, + "chipActionFilterOut": "Filtrovať", + "@chipActionFilterOut": {}, + "chipActionFilterIn": "Prefiltrovať", + "@chipActionFilterIn": {}, + "chipActionHide": "Skryť", + "@chipActionHide": {}, + "chipActionPin": "Pripnúť na začiatok", + "@chipActionPin": {}, + "chipActionUnpin": "Odstrániť z pripnutia", + "@chipActionUnpin": {}, + "chipActionRename": "Premenovať", + "@chipActionRename": {}, + "chipActionSetCover": "Nastaviť pozadie", + "@chipActionSetCover": {}, + "chipActionCreateAlbum": "Vytvoriť album", + "@chipActionCreateAlbum": {}, + "entryActionCopyToClipboard": "Skopírovať", + "@entryActionCopyToClipboard": {}, + "entryActionDelete": "Odstrániť", + "@entryActionDelete": {}, + "entryActionConvert": "Zmeniť", + "@entryActionConvert": {}, + "entryActionExport": "Exportovať", + "@entryActionExport": {}, + "entryActionRename": "Premenovať", + "@entryActionRename": {}, + "entryActionRestore": "Obnoviť", + "@entryActionRestore": {}, + "entryActionRotateCCW": "Otočiť proti smeru hodinových ručičiek", + "@entryActionRotateCCW": {}, + "entryActionFlip": "Prevrátiť horizontálne", + "@entryActionFlip": {}, + "entryActionPrint": "Vytlačiť", + "@entryActionPrint": {}, + "entryActionShareImageOnly": "Zdieľať iba obrázok", + "@entryActionShareImageOnly": {}, + "entryActionShareVideoOnly": "Zdieľať iba video", + "@entryActionShareVideoOnly": {}, + "entryActionViewSource": "Zobraziť zdroj", + "@entryActionViewSource": {}, + "entryActionShowGeoTiffOnMap": "Zobraziť na mape", + "@entryActionShowGeoTiffOnMap": {}, + "entryActionConvertMotionPhotoToStillImage": "Konvertovať na statický obrázok", + "@entryActionConvertMotionPhotoToStillImage": {}, + "entryActionViewMotionPhotoVideo": "Otvoriť video", + "@entryActionViewMotionPhotoVideo": {}, + "entryActionOpen": "Otvoriť v", + "@entryActionOpen": {}, + "entryActionSetAs": "Nastaviť ako", + "@entryActionSetAs": {}, + "entryActionInfo": "Informácie", + "@entryActionInfo": {}, + "entryActionRotateCW": "Otočiť po smere hodinových ručičiek", + "@entryActionRotateCW": {}, + "entryActionShare": "Zdieľať", + "@entryActionShare": {}, + "entryActionEdit": "Upraviť", + "@entryActionEdit": {}, + "nameConflictStrategySkip": "Preskočiť", + "@nameConflictStrategySkip": {}, + "filterTypeAnimatedLabel": "Animované", + "@filterTypeAnimatedLabel": {}, + "filterRatingRejectedLabel": "Zamietnuté", + "@filterRatingRejectedLabel": {}, + "coordinateDmsWest": "W", + "@coordinateDmsWest": {}, + "videoLoopModeNever": "Nikdy", + "@videoLoopModeNever": {}, + "mapStyleGoogleTerrain": "Google mapy (terén)", + "@mapStyleGoogleTerrain": {}, + "accessibilityAnimationsKeep": "Zachovanie efektov na obrazovke", + "@accessibilityAnimationsKeep": {}, + "displayRefreshRatePreferLowest": "Najnižšie možné", + "@displayRefreshRatePreferLowest": {}, + "moveUndatedConfirmationDialogSetDate": "Uložiť dátumy", + "@moveUndatedConfirmationDialogSetDate": {}, + "editEntryDateDialogSourceFileModifiedDate": "Dátum modifikácie súboru", + "@editEntryDateDialogSourceFileModifiedDate": {}, + "widgetDisplayedItemMostRecent": "Najnovšie", + "@widgetDisplayedItemMostRecent": {}, + "missingSystemFilePickerDialogMessage": "Systémový vyberač súborov chýba alebo je vypnutý. Povoľte ho a skúste to znova.", + "@missingSystemFilePickerDialogMessage": {}, + "moveUndatedConfirmationDialogMessage": "Uložiť dátumy pred pokračovaním?", + "@moveUndatedConfirmationDialogMessage": {}, + "hideFilterConfirmationDialogMessage": "Vybrané fotky a videá sa nebudú zobrazovať vo vašich kolekciách. Môžete ich obnoviť v nastaveniach \"Súkromie\".\n\nUrčite chcete schovať tieto súbory?", + "@hideFilterConfirmationDialogMessage": {}, + "entryActionOpenMap": "Ukázať na mape v aplikácií", + "@entryActionOpenMap": {}, + "entryActionRotateScreen": "Otočiť obrazovku", + "@entryActionRotateScreen": {}, + "entryActionAddFavourite": "Pridať do obľúbených", + "@entryActionAddFavourite": {}, + "entryActionRemoveFavourite": "Odstrániť z obľúbených", + "@entryActionRemoveFavourite": {}, + "videoActionCaptureFrame": "Zachytiť obraz", + "@videoActionCaptureFrame": {}, + "videoActionMute": "Stlmiť zvuk", + "@videoActionMute": {}, + "videoActionUnmute": "Zapnúť zvuk", + "@videoActionUnmute": {}, + "videoActionPause": "Pozastaviť", + "@videoActionPause": {}, + "videoActionPlay": "Spustiť", + "@videoActionPlay": {}, + "videoActionReplay10": "Pretočiť späť o 10 sekúnd", + "@videoActionReplay10": {}, + "videoActionSkip10": "Pretočiť dopredu o 10 sekúnd", + "@videoActionSkip10": {}, + "videoActionSelectStreams": "Výber stopy", + "@videoActionSelectStreams": {}, + "videoActionSetSpeed": "Rýchlosť prehrávania", + "@videoActionSetSpeed": {}, + "slideshowActionResume": "Pokračovať", + "@slideshowActionResume": {}, + "slideshowActionShowInCollection": "Zobraziť v kolekcií", + "@slideshowActionShowInCollection": {}, + "entryInfoActionEditDate": "Upraviť dátum a čas", + "@entryInfoActionEditDate": {}, + "entryInfoActionEditLocation": "Upraviť polohu", + "@entryInfoActionEditLocation": {}, + "entryInfoActionEditTitleDescription": "Upraviť nadpis & popis", + "@entryInfoActionEditTitleDescription": {}, + "entryInfoActionEditRating": "Upraviť hodnotenie", + "@entryInfoActionEditRating": {}, + "entryInfoActionEditTags": "Upraviť značky", + "@entryInfoActionEditTags": {}, + "entryInfoActionRemoveMetadata": "Odstrániť metadáta", + "@entryInfoActionRemoveMetadata": {}, + "entryInfoActionExportMetadata": "Exportovať metadáta", + "@entryInfoActionExportMetadata": {}, + "entryInfoActionRemoveLocation": "Odstrániť polohu", + "@entryInfoActionRemoveLocation": {}, + "filterAspectRatioLandscapeLabel": "Horizontálne", + "@filterAspectRatioLandscapeLabel": {}, + "filterAspectRatioPortraitLabel": "Vetrikálne", + "@filterAspectRatioPortraitLabel": {}, + "filterBinLabel": "Kôš", + "@filterBinLabel": {}, + "filterFavouriteLabel": "Obľúbené", + "@filterFavouriteLabel": {}, + "filterNoDateLabel": "Bez dátumu", + "@filterNoDateLabel": {}, + "filterNoAddressLabel": "Bez adresy", + "@filterNoAddressLabel": {}, + "filterNoRatingLabel": "Nehodnotené", + "@filterNoRatingLabel": {}, + "filterTaggedLabel": "Označené", + "@filterTaggedLabel": {}, + "filterNoTagLabel": "Neoznačené", + "@filterNoTagLabel": {}, + "filterNoTitleLabel": "Bez nadpisu", + "@filterNoTitleLabel": {}, + "filterOnThisDayLabel": "Tento deň", + "@filterOnThisDayLabel": {}, + "filterRecentlyAddedLabel": "Nedávno pridané", + "@filterRecentlyAddedLabel": {}, + "filterTypeMotionPhotoLabel": "Fotka v pohybe", + "@filterTypeMotionPhotoLabel": {}, + "filterTypePanoramaLabel": "Panoráma", + "@filterTypePanoramaLabel": {}, + "filterTypeRawLabel": "Raw", + "@filterTypeRawLabel": {}, + "filterTypeSphericalVideoLabel": "360° Video", + "@filterTypeSphericalVideoLabel": {}, + "filterTypeGeotiffLabel": "GeoTIFF", + "@filterTypeGeotiffLabel": {}, + "filterMimeImageLabel": "Obrázok", + "@filterMimeImageLabel": {}, + "filterMimeVideoLabel": "Video", + "@filterMimeVideoLabel": {}, + "coordinateFormatDms": "DMS", + "@coordinateFormatDms": {}, + "coordinateFormatDecimal": "Desatinné stupne", + "@coordinateFormatDecimal": {}, + "coordinateDmsNorth": "N", + "@coordinateDmsNorth": {}, + "coordinateDmsSouth": "S", + "@coordinateDmsSouth": {}, + "coordinateDmsEast": "E", + "@coordinateDmsEast": {}, + "unitSystemMetric": "Metrické", + "@unitSystemMetric": {}, + "unitSystemImperial": "Imperiálne", + "@unitSystemImperial": {}, + "videoLoopModeShortOnly": "Iba krátke videá", + "@videoLoopModeShortOnly": {}, + "videoLoopModeAlways": "Vždy", + "@videoLoopModeAlways": {}, + "videoControlsPlay": "Spustiť", + "@videoControlsPlay": {}, + "videoControlsPlaySeek": "Spustiť & pretočiť dozadu/dopredu", + "@videoControlsPlaySeek": {}, + "videoControlsPlayOutside": "Otvoriť v inom prehrávači", + "@videoControlsPlayOutside": {}, + "mapStyleGoogleNormal": "Google mapy", + "@mapStyleGoogleNormal": {}, + "mapStyleGoogleHybrid": "Google mapy (Hybridne)", + "@mapStyleGoogleHybrid": {}, + "mapStyleHuaweiNormal": "Mapy Petal", + "@mapStyleHuaweiNormal": {}, + "mapStyleHuaweiTerrain": "Mapy Petal (terén)", + "@mapStyleHuaweiTerrain": {}, + "mapStyleOsmHot": "Humanitarian OSM", + "@mapStyleOsmHot": {}, + "mapStyleStamenToner": "Stamen Toner", + "@mapStyleStamenToner": {}, + "mapStyleStamenWatercolor": "Stamen Watercolor", + "@mapStyleStamenWatercolor": {}, + "nameConflictStrategyRename": "Premenovať", + "@nameConflictStrategyRename": {}, + "videoControlsNone": "Žiadne", + "@videoControlsNone": {}, + "nameConflictStrategyReplace": "Nahradiť", + "@nameConflictStrategyReplace": {}, + "keepScreenOnNever": "Nikdy", + "@keepScreenOnNever": {}, + "keepScreenOnVideoPlayback": "Počas prehrávania", + "@keepScreenOnVideoPlayback": {}, + "keepScreenOnViewerOnly": "Iba stránka prehliadača", + "@keepScreenOnViewerOnly": {}, + "keepScreenOnAlways": "Vždy", + "@keepScreenOnAlways": {}, + "accessibilityAnimationsRemove": "Predchádzanie efektom obrazovky", + "@accessibilityAnimationsRemove": {}, + "displayRefreshRatePreferHighest": "Najvyššie možné", + "@displayRefreshRatePreferHighest": {}, + "subtitlePositionTop": "Navrchu", + "@subtitlePositionTop": {}, + "subtitlePositionBottom": "Naspodku", + "@subtitlePositionBottom": {}, + "videoPlaybackSkip": "Preskočiť", + "@videoPlaybackSkip": {}, + "videoPlaybackMuted": "Spustiť stlmené", + "@videoPlaybackMuted": {}, + "videoPlaybackWithSound": "Spustiť so zvukom", + "@videoPlaybackWithSound": {}, + "themeBrightnessLight": "Svetlá", + "@themeBrightnessLight": {}, + "themeBrightnessDark": "Tmavá", + "@themeBrightnessDark": {}, + "themeBrightnessBlack": "Čierna", + "@themeBrightnessBlack": {}, + "viewerTransitionSlide": "Snímka", + "@viewerTransitionSlide": {}, + "viewerTransitionParallax": "Parallax", + "@viewerTransitionParallax": {}, + "viewerTransitionFade": "Vyblednúť", + "@viewerTransitionFade": {}, + "viewerTransitionZoomIn": "Priblížiť", + "@viewerTransitionZoomIn": {}, + "viewerTransitionNone": "Žiadne", + "@viewerTransitionNone": {}, + "wallpaperTargetHome": "Domáca obrazovka", + "@wallpaperTargetHome": {}, + "wallpaperTargetLock": "Zamknutá obrazovka", + "@wallpaperTargetLock": {}, + "wallpaperTargetHomeLock": "Domáca a zamknutá obrazovka", + "@wallpaperTargetHomeLock": {}, + "widgetDisplayedItemRandom": "Náhodné", + "@widgetDisplayedItemRandom": {}, + "widgetOpenPageHome": "Isť domov", + "@widgetOpenPageHome": {}, + "widgetOpenPageCollection": "Otvoriť kolekciu", + "@widgetOpenPageCollection": {}, + "widgetOpenPageViewer": "Otvoriť prehliadač", + "@widgetOpenPageViewer": {}, + "albumTierNew": "Nový", + "@albumTierNew": {}, + "albumTierPinned": "Pripnuté", + "@albumTierPinned": {}, + "albumTierSpecial": "Spoločné", + "@albumTierSpecial": {}, + "albumTierApps": "Aplikácie", + "@albumTierApps": {}, + "albumTierRegular": "Ostatné", + "@albumTierRegular": {}, + "storageVolumeDescriptionFallbackPrimary": "Vnútorný ukladací priestor", + "@storageVolumeDescriptionFallbackPrimary": {}, + "storageVolumeDescriptionFallbackNonPrimary": "SD karta", + "@storageVolumeDescriptionFallbackNonPrimary": {}, + "rootDirectoryDescription": "Koreňový adresár", + "@rootDirectoryDescription": {}, + "nameConflictDialogSingleSourceMessage": "Niektoré súbory v cieľovej destinácií majú rovnaké názvy.", + "@nameConflictDialogSingleSourceMessage": {}, + "nameConflictDialogMultipleSourceMessage": "Niektoré súbory majú rovnaký názov.", + "@nameConflictDialogMultipleSourceMessage": {}, + "addShortcutDialogLabel": "Názov skratky", + "@addShortcutDialogLabel": {}, + "addShortcutButtonLabel": "PRIDAŤ", + "@addShortcutButtonLabel": {}, + "noMatchingAppDialogMessage": "Nie je podporované žiadnou aplikáciou.", + "@noMatchingAppDialogMessage": {}, + "videoResumeDialogMessage": "Pokračovať v prehrávaní od {time}?", + "@videoResumeDialogMessage": { + "placeholders": { + "time": { + "type": "String", + "example": "13:37" + } + } + }, + "videoStartOverButtonLabel": "ODZNOVA", + "@videoStartOverButtonLabel": {}, + "videoResumeButtonLabel": "POKRAČOVAŤ", + "@videoResumeButtonLabel": {}, + "setCoverDialogLatest": "Posledná položka", + "@setCoverDialogLatest": {}, + "setCoverDialogAuto": "Automaticky", + "@setCoverDialogAuto": {}, + "setCoverDialogCustom": "Vlastné", + "@setCoverDialogCustom": {}, + "newAlbumDialogTitle": "Nový album", + "@newAlbumDialogTitle": {}, + "newAlbumDialogNameLabel": "Názov albumu", + "@newAlbumDialogNameLabel": {}, + "newAlbumDialogNameLabelAlreadyExistsHelper": "Priečinok už existuje", + "@newAlbumDialogNameLabelAlreadyExistsHelper": {}, + "newAlbumDialogStorageLabel": "Úložisko:", + "@newAlbumDialogStorageLabel": {}, + "renameAlbumDialogLabel": "Nový názov", + "@renameAlbumDialogLabel": {}, + "renameAlbumDialogLabelAlreadyExistsHelper": "Priečinok už existuje", + "@renameAlbumDialogLabelAlreadyExistsHelper": {}, + "renameEntrySetPageTitle": "Premenovať", + "@renameEntrySetPageTitle": {}, + "renameEntrySetPagePatternFieldLabel": "Formát", + "@renameEntrySetPagePatternFieldLabel": {}, + "renameEntrySetPageInsertTooltip": "Vložiť položku", + "@renameEntrySetPageInsertTooltip": {}, + "renameEntrySetPagePreviewSectionTitle": "Náhľad", + "@renameEntrySetPagePreviewSectionTitle": {}, + "renameProcessorCounter": "Počítadlo", + "@renameProcessorCounter": {}, + "renameProcessorName": "Názov", + "@renameProcessorName": {}, + "exportEntryDialogFormat": "Formát:", + "@exportEntryDialogFormat": {}, + "exportEntryDialogWidth": "Šírka", + "@exportEntryDialogWidth": {}, + "exportEntryDialogHeight": "Výška", + "@exportEntryDialogHeight": {}, + "renameEntryDialogLabel": "Nový názov", + "@renameEntryDialogLabel": {}, + "editEntryDialogCopyFromItem": "Kopírovať z inej položky", + "@editEntryDialogCopyFromItem": {}, + "editEntryDialogTargetFieldsHeader": "Polia k úprave", + "@editEntryDialogTargetFieldsHeader": {}, + "editEntryDateDialogTitle": "Dátum & Čas", + "@editEntryDateDialogTitle": {}, + "editEntryDateDialogSetCustom": "Nastaviť vlastný dátum", + "@editEntryDateDialogSetCustom": {}, + "editEntryDateDialogCopyField": "Kopírovať z iného dátumu", + "@editEntryDateDialogCopyField": {}, + "editEntryDateDialogExtractFromTitle": "Extrahovať z nadpisu", + "@editEntryDateDialogExtractFromTitle": {}, + "editEntryDateDialogShift": "Posun", + "@editEntryDateDialogShift": {}, + "durationDialogHours": "Hodiny", + "@durationDialogHours": {}, + "durationDialogMinutes": "Minúty", + "@durationDialogMinutes": {}, + "durationDialogSeconds": "Sekundy", + "@durationDialogSeconds": {}, + "editEntryLocationDialogTitle": "Poloha", + "@editEntryLocationDialogTitle": {}, + "editEntryLocationDialogSetCustom": "Nastaviť vlastnú polohu", + "@editEntryLocationDialogSetCustom": {}, + "editEntryLocationDialogChooseOnMap": "Vybrať z mapy", + "@editEntryLocationDialogChooseOnMap": {}, + "viewerActionSettings": "Nastavenia", + "@viewerActionSettings": {} +} diff --git a/lib/l10n/app_th.arb b/lib/l10n/app_th.arb index 25e93a4c7..0ffe918a1 100644 --- a/lib/l10n/app_th.arb +++ b/lib/l10n/app_th.arb @@ -121,8 +121,8 @@ "@videoActionSkip10": {}, "slideshowActionShowInCollection": "แสดงคอลเลกชัน", "@slideshowActionShowInCollection": {}, - "videoActionSettings": "ตั้งค่า", - "@videoActionSettings": {}, + "viewerActionSettings": "ตั้งค่า", + "@viewerActionSettings": {}, "slideshowActionResume": "เล่นต่อ", "@slideshowActionResume": {}, "entryInfoActionEditTitleDescription": "แก้ไขชื่อและคำบรรยาย", diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 4f1945b97..6893b71fd 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -147,8 +147,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "Oynatma hızı", "@videoActionSetSpeed": {}, - "videoActionSettings": "Ayarlar", - "@videoActionSettings": {}, + "viewerActionSettings": "Ayarlar", + "@viewerActionSettings": {}, "entryInfoActionEditDate": "Tarih ve saati düzenle", "@entryInfoActionEditDate": {}, "entryInfoActionEditLocation": "Konumu düzenle", @@ -629,8 +629,6 @@ "@albumPageTitle": {}, "albumEmpty": "Albüm yok", "@albumEmpty": {}, - "createAlbumTooltip": "Albüm oluştur", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "OLUŞTUR", "@createAlbumButtonLabel": {}, "newFilterBanner": "yeni", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 3f0f8c3d4..75930c2bb 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -126,8 +126,8 @@ "@videoActionSkip10": {}, "videoActionSelectStreams": "Вибрати доріжку", "@videoActionSelectStreams": {}, - "videoActionSettings": "Налаштування", - "@videoActionSettings": {}, + "viewerActionSettings": "Налаштування", + "@viewerActionSettings": {}, "slideshowActionResume": "Продовжити", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "Показати у Колекції", @@ -885,8 +885,6 @@ "@albumPageTitle": {}, "albumEmpty": "Немає альбомів", "@albumEmpty": {}, - "createAlbumTooltip": "Створити альбом", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "СТВОРИТИ", "@createAlbumButtonLabel": {}, "countryPageTitle": "Країни", @@ -1368,5 +1366,45 @@ "tooManyItemsErrorDialogMessage": "Спробуйте ще раз з меншою кількістю елементів.", "@tooManyItemsErrorDialogMessage": {}, "settingsVideoGestureVerticalDragBrightnessVolume": "Проведіть пальцем угору або вниз, щоб налаштувати яскравість/гучність", - "@settingsVideoGestureVerticalDragBrightnessVolume": {} + "@settingsVideoGestureVerticalDragBrightnessVolume": {}, + "chipActionConfigureVault": "Налаштувати сховище", + "@chipActionConfigureVault": {}, + "vaultLockTypePassword": "Пароль", + "@vaultLockTypePassword": {}, + "newVaultDialogTitle": "Нове сховище", + "@newVaultDialogTitle": {}, + "configureVaultDialogTitle": "Налаштування сховища", + "@configureVaultDialogTitle": {}, + "vaultDialogLockModeWhenScreenOff": "Заблокувати, коли екран вимикається", + "@vaultDialogLockModeWhenScreenOff": {}, + "vaultDialogLockTypeLabel": "Тип блокування", + "@vaultDialogLockTypeLabel": {}, + "pinDialogConfirm": "Підтвердити пін-код", + "@pinDialogConfirm": {}, + "passwordDialogEnter": "Введіть пароль", + "@passwordDialogEnter": {}, + "passwordDialogConfirm": "Підтвердити пароль", + "@passwordDialogConfirm": {}, + "authenticateToUnlockVault": "Пройдіть автентифікацію, щоб розблокувати сховище", + "@authenticateToUnlockVault": {}, + "settingsConfirmationVaultDataLoss": "Показувати попередження про втрату даних сховища", + "@settingsConfirmationVaultDataLoss": {}, + "chipActionLock": "Заблокувати", + "@chipActionLock": {}, + "chipActionCreateVault": "Створити сховище", + "@chipActionCreateVault": {}, + "newVaultWarningDialogMessage": "Елементи у сховищах доступні лише для цього додатка і ні для кого іншого.\n\nЯкщо ви видалите цю програму або очистите дані програми, ви втратите всі ці елементи.", + "@newVaultWarningDialogMessage": {}, + "vaultLockTypePin": "Пін-код", + "@vaultLockTypePin": {}, + "albumTierVaults": "Сховища", + "@albumTierVaults": {}, + "authenticateToConfigureVault": "Пройдіть автентифікацію, щоб налаштувати сховище", + "@authenticateToConfigureVault": {}, + "vaultBinUsageDialogMessage": "У деяких сховищах використовується кошик.", + "@vaultBinUsageDialogMessage": {}, + "settingsDisablingBinWarningDialogMessage": "Елементи в кошику буде видалено назавжди.", + "@settingsDisablingBinWarningDialogMessage": {}, + "pinDialogEnter": "Введіть пін-код", + "@pinDialogEnter": {} } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 877892a9d..2acc34657 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -151,8 +151,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "播放速度", "@videoActionSetSpeed": {}, - "videoActionSettings": "设置", - "@videoActionSettings": {}, + "viewerActionSettings": "设置", + "@viewerActionSettings": {}, "slideshowActionResume": "继续", "@slideshowActionResume": {}, "slideshowActionShowInCollection": "在媒体集中显示", @@ -389,9 +389,9 @@ "@renameProcessorCounter": {}, "renameProcessorName": "名称", "@renameProcessorName": {}, - "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{删除此相册及其内容?} other{删除此相册及其 {count} 项内容?}}", + "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{删除此相册及其中的一个项目?} other{删除此相册及其中的 {count} 个项目?}}", "@deleteSingleAlbumConfirmationDialogMessage": {}, - "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{删除这些相册及其内容?} other{删除这些相册及其 {count} 项内容?}}", + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{删除这些相册及其中的一个项目?} other{删除这些相册及其中的 {count} 个项目?}}", "@deleteMultiAlbumConfirmationDialogMessage": {}, "exportEntryDialogFormat": "格式:", "@exportEntryDialogFormat": {}, @@ -691,8 +691,6 @@ "@albumPageTitle": {}, "albumEmpty": "无相册", "@albumEmpty": {}, - "createAlbumTooltip": "创建相册", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "创建", "@createAlbumButtonLabel": {}, "newFilterBanner": "新的", diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb index 2bd8c6010..1f07870f0 100644 --- a/lib/l10n/app_zh_Hant.arb +++ b/lib/l10n/app_zh_Hant.arb @@ -124,8 +124,8 @@ "@videoActionSelectStreams": {}, "videoActionSetSpeed": "播放速度", "@videoActionSetSpeed": {}, - "videoActionSettings": "設定", - "@videoActionSettings": {}, + "viewerActionSettings": "設定", + "@viewerActionSettings": {}, "slideshowActionResume": "繼續", "@slideshowActionResume": {}, "entryInfoActionEditLocation": "編輯位置", @@ -615,8 +615,6 @@ "@albumPageTitle": {}, "albumEmpty": "沒有相簿", "@albumEmpty": {}, - "createAlbumTooltip": "建立相簿", - "@createAlbumTooltip": {}, "createAlbumButtonLabel": "建立", "@createAlbumButtonLabel": {}, "newFilterBanner": "新的", diff --git a/lib/model/actions/chip_actions.dart b/lib/model/actions/chip_actions.dart index 28b596579..bb7dc079a 100644 --- a/lib/model/actions/chip_actions.dart +++ b/lib/model/actions/chip_actions.dart @@ -8,6 +8,7 @@ enum ChipAction { goToTagPage, reverse, hide, + lockVault, } extension ExtraChipAction on ChipAction { @@ -24,6 +25,8 @@ extension ExtraChipAction on ChipAction { return context.l10n.chipActionFilterOut; case ChipAction.hide: return context.l10n.chipActionHide; + case ChipAction.lockVault: + return context.l10n.chipActionLock; } } @@ -41,6 +44,8 @@ extension ExtraChipAction on ChipAction { return AIcons.reverse; case ChipAction.hide: return AIcons.hide; + case ChipAction.lockVault: + return AIcons.vaultLock; } } } diff --git a/lib/model/actions/chip_set_actions.dart b/lib/model/actions/chip_set_actions.dart index a1e919801..7e2efa418 100644 --- a/lib/model/actions/chip_set_actions.dart +++ b/lib/model/actions/chip_set_actions.dart @@ -12,6 +12,7 @@ enum ChipSetAction { search, toggleTitleSearch, createAlbum, + createVault, // browsing or selecting map, slideshow, @@ -21,9 +22,11 @@ enum ChipSetAction { hide, pin, unpin, + lockVault, // selecting (single filter) rename, setCover, + configureVault, } class ChipSetActions { @@ -34,15 +37,20 @@ class ChipSetActions { ChipSetAction.selectNone, ]; + // `null` items are converted to dividers static const browsing = [ ChipSetAction.search, ChipSetAction.toggleTitleSearch, - ChipSetAction.createAlbum, + null, ChipSetAction.map, ChipSetAction.slideshow, ChipSetAction.stats, + null, + ChipSetAction.createAlbum, + ChipSetAction.createVault, ]; + // `null` items are converted to dividers static const selection = [ ChipSetAction.setCover, ChipSetAction.pin, @@ -50,9 +58,13 @@ class ChipSetActions { ChipSetAction.delete, ChipSetAction.rename, ChipSetAction.hide, + null, ChipSetAction.map, ChipSetAction.slideshow, ChipSetAction.stats, + null, + ChipSetAction.configureVault, + ChipSetAction.lockVault, ]; } @@ -76,6 +88,8 @@ extension ExtraChipSetAction on ChipSetAction { return context.l10n.collectionActionShowTitleSearch; case ChipSetAction.createAlbum: return context.l10n.chipActionCreateAlbum; + case ChipSetAction.createVault: + return context.l10n.chipActionCreateVault; // browsing or selecting case ChipSetAction.map: return context.l10n.menuActionMap; @@ -92,11 +106,15 @@ extension ExtraChipSetAction on ChipSetAction { return context.l10n.chipActionPin; case ChipSetAction.unpin: return context.l10n.chipActionUnpin; + case ChipSetAction.lockVault: + return context.l10n.chipActionLock; // selecting (single filter) case ChipSetAction.rename: return context.l10n.chipActionRename; case ChipSetAction.setCover: return context.l10n.chipActionSetCover; + case ChipSetAction.configureVault: + return context.l10n.chipActionConfigureVault; } } @@ -121,6 +139,8 @@ extension ExtraChipSetAction on ChipSetAction { return AIcons.filter; case ChipSetAction.createAlbum: return AIcons.add; + case ChipSetAction.createVault: + return AIcons.vaultAdd; // browsing or selecting case ChipSetAction.map: return AIcons.map; @@ -137,11 +157,15 @@ extension ExtraChipSetAction on ChipSetAction { return AIcons.pin; case ChipSetAction.unpin: return AIcons.unpin; + case ChipSetAction.lockVault: + return AIcons.vaultLock; // selecting (single filter) case ChipSetAction.rename: return AIcons.name; case ChipSetAction.setCover: return AIcons.setCover; + case ChipSetAction.configureVault: + return AIcons.vaultConfigure; } } } diff --git a/lib/model/actions/entry_actions.dart b/lib/model/actions/entry_actions.dart index 0a653b736..880142a25 100644 --- a/lib/model/actions/entry_actions.dart +++ b/lib/model/actions/entry_actions.dart @@ -71,13 +71,15 @@ class EntryActions { ]; static const export = [ + ...exportInternal, + ...exportExternal, + ]; + + static const exportInternal = [ EntryAction.convert, EntryAction.addShortcut, EntryAction.copyToClipboard, EntryAction.print, - EntryAction.open, - EntryAction.openMap, - EntryAction.setAs, ]; static const exportExternal = [ @@ -186,7 +188,7 @@ extension ExtraEntryAction on EntryAction { case EntryAction.videoSetSpeed: return context.l10n.videoActionSetSpeed; case EntryAction.videoSettings: - return context.l10n.videoActionSettings; + return context.l10n.viewerActionSettings; case EntryAction.videoTogglePlay: // different data depending on toggle state return context.l10n.videoActionPlay; diff --git a/lib/model/actions/slideshow_actions.dart b/lib/model/actions/slideshow_actions.dart index 6e0b51fd8..55d40847f 100644 --- a/lib/model/actions/slideshow_actions.dart +++ b/lib/model/actions/slideshow_actions.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; enum SlideshowAction { resume, showInCollection, + settings, } extension ExtraSlideshowAction on SlideshowAction { @@ -14,6 +15,8 @@ extension ExtraSlideshowAction on SlideshowAction { return context.l10n.slideshowActionResume; case SlideshowAction.showInCollection: return context.l10n.slideshowActionShowInCollection; + case SlideshowAction.settings: + return context.l10n.viewerActionSettings; } } @@ -25,6 +28,8 @@ extension ExtraSlideshowAction on SlideshowAction { return AIcons.play; case SlideshowAction.showInCollection: return AIcons.allCollection; + case SlideshowAction.settings: + return AIcons.settings; } } } diff --git a/lib/model/covers.dart b/lib/model/covers.dart index dfd07b8d3..601a07bd5 100644 --- a/lib/model/covers.dart +++ b/lib/model/covers.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; @@ -38,6 +39,8 @@ class Covers { Set get all => Set.unmodifiable(_rows); Tuple3? of(CollectionFilter filter) { + if (filter is AlbumFilter && vaults.isLocked(filter.album)) return null; + final row = _rows.firstWhereOrNull((row) => row.filter == filter); return row != null ? Tuple3(row.entryId, row.packageName, row.color) : null; } diff --git a/lib/model/db/db_metadata.dart b/lib/model/db/db_metadata.dart index cc23707bf..bf4f677ae 100644 --- a/lib/model/db/db_metadata.dart +++ b/lib/model/db/db_metadata.dart @@ -5,6 +5,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/trash.dart'; +import 'package:aves/model/vaults/details.dart'; import 'package:aves/model/video_playback.dart'; abstract class MetadataDb { @@ -16,17 +17,17 @@ abstract class MetadataDb { Future reset(); - Future removeIds(Iterable ids, {Set? dataTypes}); + Future removeIds(Set ids, {Set? dataTypes}); // entries Future clearEntries(); - Future> loadEntries({String? directory}); + Future> loadEntries({int? origin, String? directory}); - Future> loadEntriesById(Iterable ids); + Future> loadEntriesById(Set ids); - Future saveEntries(Iterable entries); + Future saveEntries(Set entries); Future updateEntry(int id, AvesEntry entry); @@ -44,7 +45,7 @@ abstract class MetadataDb { Future> loadCatalogMetadata(); - Future> loadCatalogMetadataById(Iterable ids); + Future> loadCatalogMetadataById(Set ids); Future saveCatalogMetadata(Set metadataEntries); @@ -56,12 +57,24 @@ abstract class MetadataDb { Future> loadAddresses(); - Future> loadAddressesById(Iterable ids); + Future> loadAddressesById(Set ids); Future saveAddresses(Set addresses); Future updateAddress(int id, AddressDetails? address); + // vaults + + Future clearVaults(); + + Future> loadAllVaults(); + + Future addVaults(Set rows); + + Future updateVault(String oldName, VaultDetails row); + + Future removeVaults(Set rows); + // trash Future clearTrashDetails(); @@ -76,11 +89,11 @@ abstract class MetadataDb { Future> loadAllFavourites(); - Future addFavourites(Iterable rows); + Future addFavourites(Set rows); Future updateFavouriteId(int id, FavouriteRow row); - Future removeFavourites(Iterable rows); + Future removeFavourites(Set rows); // covers @@ -88,7 +101,7 @@ abstract class MetadataDb { Future> loadAllCovers(); - Future addCovers(Iterable rows); + Future addCovers(Set rows); Future updateCoverEntryId(int id, CoverRow row); @@ -104,5 +117,5 @@ abstract class MetadataDb { Future addVideoPlayback(Set rows); - Future removeVideoPlayback(Iterable ids); + Future removeVideoPlayback(Set ids); } diff --git a/lib/model/db/db_metadata_sqflite.dart b/lib/model/db/db_metadata_sqflite.dart index 2e0e3e11d..9bc6a8885 100644 --- a/lib/model/db/db_metadata_sqflite.dart +++ b/lib/model/db/db_metadata_sqflite.dart @@ -9,6 +9,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/trash.dart'; +import 'package:aves/model/vaults/details.dart'; import 'package:aves/model/video_playback.dart'; import 'package:aves/services/common/services.dart'; import 'package:collection/collection.dart'; @@ -26,6 +27,7 @@ class SqfliteMetadataDb implements MetadataDb { static const addressTable = 'address'; static const favouriteTable = 'favourites'; static const coverTable = 'covers'; + static const vaultTable = 'vaults'; static const trashTable = 'trash'; static const videoPlaybackTable = 'videoPlayback'; @@ -55,6 +57,7 @@ class SqfliteMetadataDb implements MetadataDb { ', sourceDateTakenMillis INTEGER' ', durationMillis INTEGER' ', trashed INTEGER DEFAULT 0' + ', origin INTEGER DEFAULT 0' ')'); await db.execute('CREATE TABLE $dateTakenTable(' 'id INTEGER PRIMARY KEY' @@ -89,6 +92,12 @@ class SqfliteMetadataDb implements MetadataDb { ', packageName TEXT' ', color INTEGER' ')'); + await db.execute('CREATE TABLE $vaultTable(' + 'name TEXT PRIMARY KEY' + ', autoLock INTEGER' + ', useBin INTEGER' + ', lockType TEXT' + ')'); await db.execute('CREATE TABLE $trashTable(' 'id INTEGER PRIMARY KEY' ', path TEXT' @@ -100,7 +109,7 @@ class SqfliteMetadataDb implements MetadataDb { ')'); }, onUpgrade: MetadataDbUpgrader.upgradeDb, - version: 10, + version: 11, ); final maxIdRows = await _db.rawQuery('SELECT max(id) AS maxId FROM $entryTable'); @@ -122,7 +131,7 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future removeIds(Iterable ids, {Set? dataTypes}) async { + Future removeIds(Set ids, {Set? dataTypes}) async { if (ids.isEmpty) return; final _dataTypes = dataTypes ?? EntryDataType.values.toSet(); @@ -162,15 +171,23 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future> loadEntries({String? directory}) async { + Future> loadEntries({int? origin, String? directory}) async { + String? where; + final whereArgs = []; + + if (origin != null) { + where = 'origin = ?'; + whereArgs.add(origin); + } + if (directory != null) { final separator = pContext.separator; if (!directory.endsWith(separator)) { directory = '$directory$separator'; } - const where = 'path LIKE ?'; - final whereArgs = ['$directory%']; + where = '${where != null ? '$where AND ' : ''}path LIKE ?'; + whereArgs.add('$directory%'); final rows = await _db.query(entryTable, where: where, whereArgs: whereArgs); final dirLength = directory.length; @@ -184,15 +201,15 @@ class SqfliteMetadataDb implements MetadataDb { .toSet(); } - final rows = await _db.query(entryTable); + final rows = await _db.query(entryTable, where: where, whereArgs: whereArgs); return rows.map(AvesEntry.fromMap).toSet(); } @override - Future> loadEntriesById(Iterable ids) => _getByIds(ids, entryTable, AvesEntry.fromMap); + Future> loadEntriesById(Set ids) => _getByIds(ids, entryTable, AvesEntry.fromMap); @override - Future saveEntries(Iterable entries) async { + Future saveEntries(Set entries) async { if (entries.isEmpty) return; final stopwatch = Stopwatch()..start(); final batch = _db.batch(); @@ -258,7 +275,7 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future> loadCatalogMetadataById(Iterable ids) => _getByIds(ids, metadataTable, CatalogMetadata.fromMap); + Future> loadCatalogMetadataById(Set ids) => _getByIds(ids, metadataTable, CatalogMetadata.fromMap); @override Future saveCatalogMetadata(Set metadataEntries) async { @@ -317,7 +334,7 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future> loadAddressesById(Iterable ids) => _getByIds(ids, addressTable, AddressDetails.fromMap); + Future> loadAddressesById(Set ids) => _getByIds(ids, addressTable, AddressDetails.fromMap); @override Future saveAddresses(Set addresses) async { @@ -346,6 +363,54 @@ class SqfliteMetadataDb implements MetadataDb { ); } + // vaults + + @override + Future clearVaults() async { + final count = await _db.delete(vaultTable, where: '1'); + debugPrint('$runtimeType clearVaults deleted $count rows'); + } + + @override + Future> loadAllVaults() async { + final rows = await _db.query(vaultTable); + return rows.map(VaultDetails.fromMap).toSet(); + } + + @override + Future addVaults(Set rows) async { + if (rows.isEmpty) return; + final batch = _db.batch(); + rows.forEach((row) => _batchInsertVault(batch, row)); + await batch.commit(noResult: true); + } + + @override + Future updateVault(String oldName, VaultDetails row) async { + final batch = _db.batch(); + batch.delete(vaultTable, where: 'name = ?', whereArgs: [oldName]); + _batchInsertVault(batch, row); + await batch.commit(noResult: true); + } + + void _batchInsertVault(Batch batch, VaultDetails row) { + batch.insert( + vaultTable, + row.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + @override + Future removeVaults(Set rows) async { + if (rows.isEmpty) return; + + // using array in `whereArgs` and using it with `where id IN ?` is a pain, so we prefer `batch` instead + final batch = _db.batch(); + rows.map((v) => v.name).forEach((name) => batch.delete(vaultTable, where: 'name = ?', whereArgs: [name])); + await batch.commit(noResult: true); + } + // trash @override @@ -392,7 +457,7 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future addFavourites(Iterable rows) async { + Future addFavourites(Set rows) async { if (rows.isEmpty) return; final batch = _db.batch(); rows.forEach((row) => _batchInsertFavourite(batch, row)); @@ -416,7 +481,7 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future removeFavourites(Iterable rows) async { + Future removeFavourites(Set rows) async { if (rows.isEmpty) return; final ids = rows.map((row) => row.entryId); if (ids.isEmpty) return; @@ -442,7 +507,7 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future addCovers(Iterable rows) async { + Future addCovers(Set rows) async { if (rows.isEmpty) return; final batch = _db.batch(); @@ -532,7 +597,7 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future removeVideoPlayback(Iterable ids) async { + Future removeVideoPlayback(Set ids) async { if (ids.isEmpty) return; // using array in `whereArgs` and using it with `where filter IN ?` is a pain, so we prefer `batch` instead @@ -543,7 +608,7 @@ class SqfliteMetadataDb implements MetadataDb { // convenience methods - Future> _getByIds(Iterable ids, String table, T Function(Map row) mapRow) async { + Future> _getByIds(Set ids, String table, T Function(Map row) mapRow) async { if (ids.isEmpty) return {}; final rows = await _db.query( table, diff --git a/lib/model/db/db_metadata_sqflite_upgrade.dart b/lib/model/db/db_metadata_sqflite_upgrade.dart index 5d38540a8..f2840e2ee 100644 --- a/lib/model/db/db_metadata_sqflite_upgrade.dart +++ b/lib/model/db/db_metadata_sqflite_upgrade.dart @@ -10,6 +10,7 @@ class MetadataDbUpgrader { static const addressTable = SqfliteMetadataDb.addressTable; static const favouriteTable = SqfliteMetadataDb.favouriteTable; static const coverTable = SqfliteMetadataDb.coverTable; + static const vaultTable = SqfliteMetadataDb.vaultTable; static const trashTable = SqfliteMetadataDb.trashTable; static const videoPlaybackTable = SqfliteMetadataDb.videoPlaybackTable; @@ -45,6 +46,9 @@ class MetadataDbUpgrader { case 9: await _upgradeFrom9(db); break; + case 10: + await _upgradeFrom10(db); + break; } oldVersion++; } @@ -370,4 +374,17 @@ class MetadataDbUpgrader { }); await batch.commit(noResult: true); } + + static Future _upgradeFrom10(Database db) async { + debugPrint('upgrading DB from v10'); + + await db.execute('ALTER TABLE $entryTable ADD COLUMN origin INTEGER DEFAULT 0;'); + + await db.execute('CREATE TABLE $vaultTable(' + 'name TEXT PRIMARY KEY' + ', autoLock INTEGER' + ', useBin INTEGER' + ', lockType TEXT' + ')'); + } } diff --git a/lib/model/device.dart b/lib/model/device.dart index b69195128..e49802885 100644 --- a/lib/model/device.dart +++ b/lib/model/device.dart @@ -1,16 +1,20 @@ import 'package:aves/services/common/services.dart'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:local_auth/local_auth.dart'; import 'package:package_info_plus/package_info_plus.dart'; final Device device = Device._private(); class Device { late final String _userAgent; - late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper; + late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint; + late final bool _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto; late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode; String get userAgent => _userAgent; + bool get canAuthenticateUser => _canAuthenticateUser; + bool get canGrantDirectoryAccess => _canGrantDirectoryAccess; bool get canPinShortcut => _canPinShortcut; @@ -23,6 +27,10 @@ class Device { bool get canSetLockScreenWallpaper => _canSetLockScreenWallpaper; + bool get canUseCrypto => _canUseCrypto; + + bool get canUseVaults => canAuthenticateUser || canUseCrypto; + bool get hasGeocoder => _hasGeocoder; bool get isDynamicColorAvailable => _isDynamicColorAvailable; @@ -42,6 +50,9 @@ class Device { final androidInfo = await DeviceInfoPlugin().androidInfo; _isTelevision = androidInfo.systemFeatures.contains('android.software.leanback'); + final auth = LocalAuthentication(); + _canAuthenticateUser = await auth.canCheckBiometrics || await auth.isDeviceSupported(); + final capabilities = await deviceService.getCapabilities(); _canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false; _canPinShortcut = capabilities['canPinShortcut'] ?? false; @@ -49,6 +60,7 @@ class Device { _canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false; _canRequestManageMedia = capabilities['canRequestManageMedia'] ?? false; _canSetLockScreenWallpaper = capabilities['canSetLockScreenWallpaper'] ?? false; + _canUseCrypto = capabilities['canUseCrypto'] ?? false; _hasGeocoder = capabilities['hasGeocoder'] ?? false; _isDynamicColorAvailable = capabilities['isDynamicColorAvailable'] ?? false; _showPinShortcutFeedback = capabilities['showPinShortcutFeedback'] ?? false; diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 6a9dc3625..794ed6f6e 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -20,6 +20,7 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/services/geocoding_service.dart'; import 'package:aves/services/metadata/svg_metadata_service.dart'; import 'package:aves/theme/format.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/utils/time_utils.dart'; import 'package:collection/collection.dart'; @@ -29,6 +30,13 @@ import 'package:latlong2/latlong.dart'; enum EntryDataType { basic, aspectRatio, catalog, address, references } +class EntryOrigins { + static const int mediaStoreContent = 0; + static const int unknownContent = 1; + static const int file = 2; + static const int vault = 3; +} + class AvesEntry { // `sizeBytes`, `dateModifiedSecs` can be missing in viewer mode int id; @@ -40,6 +48,7 @@ class AvesEntry { int width, height, sourceRotationDegrees; int? sizeBytes, dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis; bool trashed; + int origin; int? _catalogDateMillis; CatalogMetadata? _catalogMetadata; @@ -67,6 +76,7 @@ class AvesEntry { required this.sourceDateTakenMillis, required int? durationMillis, required this.trashed, + required this.origin, this.burstEntries, }) : id = id ?? 0 { this.path = path; @@ -87,6 +97,7 @@ class AvesEntry { String? title, int? dateAddedSecs, int? dateModifiedSecs, + int? origin, List? burstEntries, }) { final copyEntryId = id ?? this.id; @@ -107,6 +118,7 @@ class AvesEntry { sourceDateTakenMillis: sourceDateTakenMillis, durationMillis: durationMillis, trashed: trashed, + origin: origin ?? this.origin, burstEntries: burstEntries ?? this.burstEntries, ) ..catalogMetadata = _catalogMetadata?.copyWith(id: copyEntryId) @@ -135,6 +147,7 @@ class AvesEntry { sourceDateTakenMillis: map['sourceDateTakenMillis'] as int?, durationMillis: map['durationMillis'] as int?, trashed: (map['trashed'] as int? ?? 0) != 0, + origin: map['origin'] as int, ); } @@ -156,6 +169,7 @@ class AvesEntry { 'sourceDateTakenMillis': sourceDateTakenMillis, 'durationMillis': durationMillis, 'trashed': trashed ? 1 : 0, + 'origin': origin, }; } @@ -173,6 +187,7 @@ class AvesEntry { 'sizeBytes': sizeBytes, 'trashed': trashed, 'trashPath': trashDetails?.path, + 'origin': origin, }; } @@ -281,7 +296,9 @@ class AvesEntry { bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains); - bool get canEdit => !settings.isReadOnly && path != null && !trashed && isMediaStoreContent; + bool get isVaultContent => path?.startsWith(androidFileUtils.vaultRoot) ?? false; + + bool get canEdit => !settings.isReadOnly && path != null && !trashed && (isMediaStoreContent || isVaultContent); bool get canEditDate => canEdit && (canEditExif || canEditXmp); diff --git a/lib/model/favourites.dart b/lib/model/favourites.dart index edfb2406e..d994e31cc 100644 --- a/lib/model/favourites.dart +++ b/lib/model/favourites.dart @@ -26,7 +26,7 @@ class Favourites with ChangeNotifier { FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(entryId: entry.id); Future add(Set entries) async { - final newRows = entries.map(_entryToRow); + final newRows = entries.map(_entryToRow).toSet(); await metadataDb.addFavourites(newRows); _rows.addAll(newRows); diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index e9b1722df..226436bbf 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -73,6 +73,7 @@ class AlbumFilter extends CoveredCollectionFilter { final albumType = covers.effectiveAlbumType(album); switch (albumType) { case AlbumType.regular: + case AlbumType.vault: break; case AlbumType.app: final appColor = colors.appColor(album); diff --git a/lib/model/multipage.dart b/lib/model/multipage.dart index e0d8159ce..b28945784 100644 --- a/lib/model/multipage.dart +++ b/lib/model/multipage.dart @@ -107,6 +107,7 @@ class MultiPageInfo { sourceDateTakenMillis: mainEntry.sourceDateTakenMillis, durationMillis: pageInfo.durationMillis ?? mainEntry.durationMillis, trashed: trashed, + origin: mainEntry.origin, ) ..catalogMetadata = mainEntry.catalogMetadata?.copyWith( mimeType: pageInfo.mimeType, diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index fb2273e6d..13d04e8fd 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -31,10 +31,7 @@ class SettingsDefaults { static const keepScreenOn = KeepScreenOn.viewerOnly; static const homePage = HomePageSetting.collection; static const enableBottomNavigationBar = true; - static const confirmDeleteForever = true; - static const confirmMoveToBin = true; - static const confirmMoveUndatedItems = true; - static const confirmAfterMoveToBin = true; + static const confirm = true; static const setMetadataDateBeforeFileOp = false; static final drawerTypeBookmarks = [ null, diff --git a/lib/model/settings/enums/enums.dart b/lib/model/settings/enums/enums.dart index 96f7fd39d..ac4584d5d 100644 --- a/lib/model/settings/enums/enums.dart +++ b/lib/model/settings/enums/enums.dart @@ -6,7 +6,7 @@ enum AvesThemeBrightness { system, light, dark, black } enum AvesThemeColorMode { monochrome, polychrome } -enum ConfirmationDialog { deleteForever, moveToBin, moveUndatedItems } +enum ConfirmationDialog { createVault, deleteForever, moveToBin, moveUndatedItems } enum CoordinateFormat { dms, decimal } diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index f2595ffae..ec3584ff1 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -30,6 +30,7 @@ import 'package:latlong2/latlong.dart'; final Settings settings = Settings._private(); class Settings extends ChangeNotifier { + final List _subscriptions = []; final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change'); final StreamController _updateStreamController = StreamController.broadcast(); @@ -40,7 +41,7 @@ class Settings extends ChangeNotifier { static const int _recentFilterHistoryMax = 10; static const Set _internalKeys = { hasAcceptedTermsKey, - catalogTimeZoneKey, + catalogTimeZoneRawOffsetMillisKey, searchHistoryKey, platformAccelerometerRotationKey, platformTransitionAnimationScaleKey, @@ -56,7 +57,7 @@ class Settings extends ChangeNotifier { static const isInstalledAppAccessAllowedKey = 'is_installed_app_access_allowed'; static const isErrorReportingAllowedKey = 'is_crashlytics_enabled'; static const localeKey = 'locale'; - static const catalogTimeZoneKey = 'catalog_time_zone'; + static const catalogTimeZoneRawOffsetMillisKey = 'catalog_time_zone_raw_offset_millis'; static const tileExtentPrefixKey = 'tile_extent_'; static const tileLayoutPrefixKey = 'tile_layout_'; static const entryRenamingPatternKey = 'entry_renaming_pattern'; @@ -77,6 +78,7 @@ class Settings extends ChangeNotifier { static const keepScreenOnKey = 'keep_screen_on'; static const homePageKey = 'home_page'; static const enableBottomNavigationBarKey = 'show_bottom_navigation_bar'; + static const confirmCreateVaultKey = 'confirm_create_vault'; static const confirmDeleteForeverKey = 'confirm_delete_forever'; static const confirmMoveToBinKey = 'confirm_move_to_bin'; static const confirmMoveUndatedItemsKey = 'confirm_move_undated_items'; @@ -151,6 +153,7 @@ class Settings extends ChangeNotifier { // tag editor static const tagEditorCurrentFilterSectionExpandedKey = 'tag_editor_current_filter_section_expanded'; + static const tagEditorExpandedSectionKey = 'tag_editor_expanded_section'; // map static const mapStyleKey = 'info_map_style'; @@ -209,7 +212,10 @@ class Settings extends ChangeNotifier { await settingsStore.init(); _appliedLocale = null; if (monitorPlatformSettings) { - _platformSettingsChangeChannel.receiveBroadcastStream().listen((event) => _onPlatformSettingsChanged(event as Map?)); + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + _subscriptions.add(_platformSettingsChangeChannel.receiveBroadcastStream().listen((event) => _onPlatformSettingsChanged(event as Map?))); } } @@ -277,10 +283,10 @@ class Settings extends ChangeNotifier { } Future sanitize() async { - if (timeToTakeAction == AccessibilityTimeout.system && !(await AccessibilityService.hasRecommendedTimeouts())) { + if (timeToTakeAction == AccessibilityTimeout.system && !await AccessibilityService.hasRecommendedTimeouts()) { _set(timeToTakeActionKey, null); } - if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !(await windowService.isCutoutAware())) { + if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !await windowService.isCutoutAware()) { _set(viewerUseCutoutKey, null); } } @@ -356,9 +362,9 @@ class Settings extends ChangeNotifier { return _appliedLocale!; } - String get catalogTimeZone => getString(catalogTimeZoneKey) ?? ''; + int get catalogTimeZoneRawOffsetMillis => getInt(catalogTimeZoneRawOffsetMillisKey) ?? 0; - set catalogTimeZone(String newValue) => _set(catalogTimeZoneKey, newValue); + set catalogTimeZoneRawOffsetMillis(int newValue) => _set(catalogTimeZoneRawOffsetMillisKey, newValue); double getTileExtent(String routeName) => getDouble(tileExtentPrefixKey + routeName) ?? 0; @@ -432,19 +438,23 @@ class Settings extends ChangeNotifier { set enableBottomNavigationBar(bool newValue) => _set(enableBottomNavigationBarKey, newValue); - bool get confirmDeleteForever => getBool(confirmDeleteForeverKey) ?? SettingsDefaults.confirmDeleteForever; + bool get confirmCreateVault => getBool(confirmCreateVaultKey) ?? SettingsDefaults.confirm; + + set confirmCreateVault(bool newValue) => _set(confirmCreateVaultKey, newValue); + + bool get confirmDeleteForever => getBool(confirmDeleteForeverKey) ?? SettingsDefaults.confirm; set confirmDeleteForever(bool newValue) => _set(confirmDeleteForeverKey, newValue); - bool get confirmMoveToBin => getBool(confirmMoveToBinKey) ?? SettingsDefaults.confirmMoveToBin; + bool get confirmMoveToBin => getBool(confirmMoveToBinKey) ?? SettingsDefaults.confirm; set confirmMoveToBin(bool newValue) => _set(confirmMoveToBinKey, newValue); - bool get confirmMoveUndatedItems => getBool(confirmMoveUndatedItemsKey) ?? SettingsDefaults.confirmMoveUndatedItems; + bool get confirmMoveUndatedItems => getBool(confirmMoveUndatedItemsKey) ?? SettingsDefaults.confirm; set confirmMoveUndatedItems(bool newValue) => _set(confirmMoveUndatedItemsKey, newValue); - bool get confirmAfterMoveToBin => getBool(confirmAfterMoveToBinKey) ?? SettingsDefaults.confirmAfterMoveToBin; + bool get confirmAfterMoveToBin => getBool(confirmAfterMoveToBinKey) ?? SettingsDefaults.confirm; set confirmAfterMoveToBin(bool newValue) => _set(confirmAfterMoveToBinKey, newValue); @@ -698,6 +708,10 @@ class Settings extends ChangeNotifier { set tagEditorCurrentFilterSectionExpanded(bool newValue) => _set(tagEditorCurrentFilterSectionExpandedKey, newValue); + String? get tagEditorExpandedSection => getString(tagEditorExpandedSectionKey); + + set tagEditorExpandedSection(String? newValue) => _set(tagEditorExpandedSectionKey, newValue); + // map EntryMapStyle? get mapStyle { @@ -1010,6 +1024,7 @@ class Settings extends ChangeNotifier { case enableBlurEffectKey: case enableBottomNavigationBarKey: case mustBackTwiceToExitKey: + case confirmCreateVaultKey: case confirmDeleteForeverKey: case confirmMoveToBinKey: case confirmMoveUndatedItemsKey: @@ -1076,6 +1091,7 @@ class Settings extends ChangeNotifier { case videoControlsKey: case subtitleTextAlignmentKey: case subtitleTextPositionKey: + case tagEditorExpandedSectionKey: case mapStyleKey: case mapDefaultCenterKey: case coordinateFormatKey: diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index 6099639a6..fae997994 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.dart @@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/collection_utils.dart'; @@ -61,8 +62,10 @@ mixin AlbumMixin on SourceBase { } void updateDirectories() { - final visibleDirectories = visibleEntries.map((entry) => entry.directory).toSet(); - addDirectories(albums: visibleDirectories); + addDirectories(albums: { + ...visibleEntries.map((entry) => entry.directory), + ...vaults.all.map((v) => v.path), + }); cleanEmptyAlbums(); } @@ -73,25 +76,24 @@ mixin AlbumMixin on SourceBase { } } - void cleanEmptyAlbums([Set? albums]) { - final emptyAlbums = (albums ?? _directories).where((v) => _isEmptyAlbum(v) && !_newAlbums.contains(v)).toSet(); - if (emptyAlbums.isNotEmpty) { - _directories.removeAll(emptyAlbums); + void cleanEmptyAlbums([Set? albums]) { + final removableAlbums = (albums ?? _directories).where(_isRemovable).toSet(); + if (removableAlbums.isNotEmpty) { + _directories.removeAll(removableAlbums); _onAlbumChanged(); - invalidateAlbumFilterSummary(directories: emptyAlbums); + invalidateAlbumFilterSummary(directories: removableAlbums); final bookmarks = settings.drawerAlbumBookmarks; - final pinnedFilters = settings.pinnedFilters; - emptyAlbums.forEach((album) { + removableAlbums.forEach((album) { bookmarks?.remove(album); - pinnedFilters.removeWhere((filter) => filter is AlbumFilter && filter.album == album); }); settings.drawerAlbumBookmarks = bookmarks; - settings.pinnedFilters = pinnedFilters; } } - bool _isEmptyAlbum(String? album) => !visibleEntries.any((entry) => entry.directory == album); + bool _isRemovable(String album) { + return !(visibleEntries.any((entry) => entry.directory == album) || _newAlbums.contains(album) || vaults.isVault(album)); + } // filter summary @@ -169,8 +171,8 @@ mixin AlbumMixin on SourceBase { final separator = pContext.separator; assert(!dirPath.endsWith(separator)); + final type = androidFileUtils.getAlbumType(dirPath); if (context != null) { - final type = androidFileUtils.getAlbumType(dirPath); switch (type) { case AlbumType.camera: return context.l10n.albumCamera; @@ -183,11 +185,14 @@ mixin AlbumMixin on SourceBase { case AlbumType.videoCaptures: return context.l10n.albumVideoCaptures; case AlbumType.regular: + case AlbumType.vault: case AlbumType.app: break; } } + if (type == AlbumType.vault) return pContext.basename(dirPath); + final dir = VolumeRelativeDirectory.fromPath(dirPath); if (dir == null) return dirPath; diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 41599a7f9..fcb163e1c 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -18,6 +18,7 @@ import 'package:aves/model/source/events.dart'; import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/model/source/trash.dart'; +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/analysis_service.dart'; import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/services.dart'; @@ -60,9 +61,14 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM final oldValue = event.oldValue; if (oldValue is List?) { final oldHiddenFilters = (oldValue ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); - _onFilterVisibilityChanged(oldHiddenFilters, settings.hiddenFilters); + final newlyVisibleFilters = oldHiddenFilters.whereNot(settings.hiddenFilters.contains).toSet(); + _onFilterVisibilityChanged(newlyVisibleFilters); } }); + vaults.addListener(() { + final newlyVisibleFilters = vaults.vaultDirectories.whereNot(vaults.isLocked).map((v) => AlbumFilter(v, null)).toSet(); + _onFilterVisibilityChanged(newlyVisibleFilters); + }); } final EventBus _eventBus = EventBus(); @@ -108,16 +114,22 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM _savedDates = Map.unmodifiable(await metadataDb.loadDates()); } + Set _getAppHiddenFilters() => { + ...settings.hiddenFilters, + ...vaults.vaultDirectories.where(vaults.isLocked).map((v) => AlbumFilter(v, null)), + }; + Iterable _applyHiddenFilters(Iterable entries) { final hiddenFilters = { TrashFilter.instance, - ...settings.hiddenFilters, + ..._getAppHiddenFilters(), }; return entries.where((entry) => !hiddenFilters.any((filter) => filter.test(entry))); } Iterable _applyTrashFilter(Iterable entries) { - return entries.where(TrashFilter.instance.test); + final hiddenFilters = _getAppHiddenFilters(); + return entries.where(TrashFilter.instance.test).where((entry) => !hiddenFilters.any((filter) => filter.test(entry))); } void _invalidate({Set? entries, bool notify = true}) { @@ -198,23 +210,24 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM Future _moveEntry(AvesEntry entry, Map newFields, {required bool persist}) async { newFields.keys.forEach((key) { + final newValue = newFields[key]; switch (key) { case 'contentId': - entry.contentId = newFields['contentId'] as int?; + 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 = newFields['dateModifiedSecs'] as int?; + entry.dateModifiedSecs = newValue as int?; break; case 'path': - entry.path = newFields['path'] as String?; + entry.path = newValue as String?; break; case 'title': - entry.sourceTitle = newFields['title'] as String?; + entry.sourceTitle = newValue as String?; break; case 'trashed': - final trashed = newFields['trashed'] as bool; + final trashed = newValue as bool; entry.trashed = trashed; entry.trashDetails = trashed ? TrashDetails( @@ -225,7 +238,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM : null; break; case 'uri': - entry.uri = newFields['uri'] as String; + entry.uri = newValue as String; + break; + case 'origin': + entry.origin = newValue as int; break; } }); @@ -251,6 +267,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM final bookmark = settings.drawerAlbumBookmarks?.indexOf(sourceAlbum); final pinned = settings.pinnedFilters.contains(oldFilter); + if (vaults.isVault(sourceAlbum)) { + await vaults.rename(sourceAlbum, destinationAlbum); + } + final existingCover = covers.of(oldFilter); await covers.set( filter: newFilter, @@ -266,6 +286,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM destinationAlbums: {destinationAlbum}, movedOps: movedOps, ); + // restore bookmark and pin, as the obsolete album got removed and its associated state cleaned if (bookmark != null && bookmark != -1) { settings.drawerAlbumBookmarks = settings.drawerAlbumBookmarks?..insert(bookmark, destinationAlbum); @@ -312,6 +333,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM title: newFields['title'] as String?, dateAddedSecs: newFields['dateAddedSecs'] as int?, dateModifiedSecs: newFields['dateModifiedSecs'] as int?, + origin: newFields['origin'] as int?, )); } else { debugPrint('failed to find source entry with uri=$sourceUri'); @@ -345,7 +367,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM break; case MoveType.move: case MoveType.export: - cleanEmptyAlbums(fromAlbums); + cleanEmptyAlbums(fromAlbums.whereNotNull().toSet()); addDirectories(albums: destinationAlbums); break; case MoveType.toBin: @@ -507,11 +529,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM return recentEntry(filter); } - void _onFilterVisibilityChanged(Set oldHiddenFilters, Set currentHiddenFilters) { + void _onFilterVisibilityChanged(Set newlyVisibleFilters) { updateDerivedFilters(); eventBus.fire(const FilterVisibilityChangedEvent()); - final newlyVisibleFilters = oldHiddenFilters.whereNot(currentHiddenFilters.contains).toSet(); if (newlyVisibleFilters.isNotEmpty) { final candidateEntries = visibleEntries.where((entry) => newlyVisibleFilters.any((f) => f.test(entry))).toSet(); analyze(null, entries: candidateEntries); diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 3bf804a3a..67bfaa69d 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -8,6 +8,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:collection/collection.dart'; @@ -44,17 +45,18 @@ class MediaStoreSource extends CollectionSource { final stopwatch = Stopwatch()..start(); state = SourceState.loading; await metadataDb.init(); + await vaults.init(); await favourites.init(); await covers.init(); - final currentTimeZone = await deviceService.getDefaultTimeZone(); - if (currentTimeZone != null) { - final catalogTimeZone = settings.catalogTimeZone; - if (currentTimeZone != catalogTimeZone) { + final currentTimeZoneOffset = await deviceService.getDefaultTimeZoneRawOffsetMillis(); + if (currentTimeZoneOffset != null) { + final catalogTimeZoneOffset = settings.catalogTimeZoneRawOffsetMillis; + if (currentTimeZoneOffset != catalogTimeZoneOffset) { // clear catalog metadata to get correct date/times when moving to a different time zone debugPrint('$runtimeType clear catalog metadata to get correct date/times'); await metadataDb.clearDates(); await metadataDb.clearCatalogMetadata(); - settings.catalogTimeZone = currentTimeZone; + settings.catalogTimeZoneRawOffsetMillis = currentTimeZoneOffset; } } await loadDates(); @@ -74,7 +76,7 @@ class MediaStoreSource extends CollectionSource { final Set topEntries = {}; if (loadTopEntriesFirst) { - final topIds = settings.topEntryIds; + final topIds = settings.topEntryIds?.toSet(); if (topIds != null) { debugPrint('$runtimeType refresh ${stopwatch.elapsed} load ${topIds.length} top entries'); topEntries.addAll(await metadataDb.loadEntriesById(topIds)); @@ -83,7 +85,7 @@ class MediaStoreSource extends CollectionSource { } debugPrint('$runtimeType refresh ${stopwatch.elapsed} fetch known entries'); - final knownEntries = await metadataDb.loadEntries(directory: directory); + final knownEntries = await metadataDb.loadEntries(origin: EntryOrigins.mediaStoreContent, directory: directory); final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet(); debugPrint('$runtimeType refresh ${stopwatch.elapsed} check obsolete entries'); @@ -103,6 +105,8 @@ class MediaStoreSource extends CollectionSource { // with items that may be hidden right away because of their metadata addEntries(knownEntries, notify: false); + await _addVaultEntries(directory); + debugPrint('$runtimeType refresh ${stopwatch.elapsed} load metadata'); if (directory != null) { final ids = knownLiveEntries.map((entry) => entry.id).toSet(); @@ -129,7 +133,7 @@ class MediaStoreSource extends CollectionSource { // clean up obsolete entries if (removedEntries.isNotEmpty) { debugPrint('$runtimeType refresh ${stopwatch.elapsed} remove obsolete entries'); - await metadataDb.removeIds(removedEntries.map((entry) => entry.id)); + await metadataDb.removeIds(removedEntries.map((entry) => entry.id).toSet()); } // verify paths because some apps move files without updating their `last modified date` @@ -274,6 +278,36 @@ class MediaStoreSource extends CollectionSource { await refreshEntries(entriesToRefresh, EntryDataType.values.toSet()); } + await _refreshVaultEntries(changedUris.where(vaults.isVaultEntryUri).toSet()); + return tempUris; } + + // vault + + Future _addVaultEntries(String? directory) async { + addEntries(await metadataDb.loadEntries(origin: EntryOrigins.vault, directory: directory)); + } + + Future _refreshVaultEntries(Set changedUris) async { + final entriesToRefresh = {}; + final existingDirectories = {}; + + for (final uri in changedUris) { + final existingEntry = allEntries.firstWhereOrNull((entry) => entry.uri == uri); + if (existingEntry != null) { + entriesToRefresh.add(existingEntry); + final existingDirectory = existingEntry.directory; + if (existingDirectory != null) { + existingDirectories.add(existingDirectory); + } + } + } + + invalidateAlbumFilterSummary(directories: existingDirectories); + + if (entriesToRefresh.isNotEmpty) { + await refreshEntries(entriesToRefresh, EntryDataType.values.toSet()); + } + } } diff --git a/lib/model/vaults/details.dart b/lib/model/vaults/details.dart new file mode 100644 index 000000000..d4898148d --- /dev/null +++ b/lib/model/vaults/details.dart @@ -0,0 +1,57 @@ +import 'package:aves/model/vaults/enums.dart'; +import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/utils/collection_utils.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; + +@immutable +class VaultDetails extends Equatable { + final String name; + final bool autoLockScreenOff, useBin; + final VaultLockType lockType; + + @override + List get props => [name, autoLockScreenOff, useBin, lockType]; + + const VaultDetails({ + required this.name, + required this.autoLockScreenOff, + required this.useBin, + required this.lockType, + }); + + VaultDetails copyWith({ + String? name, + }) { + return VaultDetails( + name: name ?? this.name, + autoLockScreenOff: autoLockScreenOff, + useBin: useBin, + lockType: lockType, + ); + } + + factory VaultDetails.fromMap(Map map) { + return VaultDetails( + name: map['name'] as String, + autoLockScreenOff: (map['autoLock'] as int? ?? 0) != 0, + useBin: (map['useBin'] as int? ?? 0) != 0, + lockType: VaultLockType.values.safeByName(map['lockType'] as String, VaultLockType.system), + ); + } + + Map toMap() => { + 'name': name, + 'autoLock': autoLockScreenOff ? 1 : 0, + 'useBin': useBin ? 1 : 0, + 'lockType': lockType.name, + }; + + String get passKey => 'vault_pass_$name'; + + String get path => '${androidFileUtils.vaultRoot}$name'; + + static String? nameFromPath(String path) { + return path.startsWith(androidFileUtils.vaultRoot) ? path.substring(androidFileUtils.vaultRoot.length) : null; + } +} diff --git a/lib/model/vaults/enums.dart b/lib/model/vaults/enums.dart new file mode 100644 index 000000000..339f4c9f7 --- /dev/null +++ b/lib/model/vaults/enums.dart @@ -0,0 +1,17 @@ +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/widgets.dart'; + +enum VaultLockType { system, pin, password } + +extension ExtraVaultLockType on VaultLockType { + String getText(BuildContext context) { + switch (this) { + case VaultLockType.system: + return context.l10n.settingsSystemDefault; + case VaultLockType.pin: + return context.l10n.vaultLockTypePin; + case VaultLockType.password: + return context.l10n.vaultLockTypePassword; + } + } +} diff --git a/lib/model/vaults/vaults.dart b/lib/model/vaults/vaults.dart new file mode 100644 index 000000000..113ad7515 --- /dev/null +++ b/lib/model/vaults/vaults.dart @@ -0,0 +1,241 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:aves/model/vaults/details.dart'; +import 'package:aves/model/vaults/enums.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves/widgets/dialogs/filter_editors/password_dialog.dart'; +import 'package:aves/widgets/dialogs/filter_editors/pin_dialog.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:local_auth/error_codes.dart' as auth_error; +import 'package:local_auth/local_auth.dart'; +import 'package:screen_state/screen_state.dart'; + +final Vaults vaults = Vaults._private(); + +class Vaults extends ChangeNotifier { + final List _subscriptions = []; + Set _rows = {}; + final Set _unlockedDirPaths = {}; + + Vaults._private(); + + Future init() async { + _rows = await metadataDb.loadAllVaults(); + _vaultDirPaths = null; + final screenStateStream = Platform.isAndroid ? Screen().screenStateStream : null; + if (screenStateStream != null) { + _subscriptions.add(screenStateStream.where((event) => event == ScreenStateEvent.SCREEN_OFF).listen((event) => _onScreenOff())); + } + } + + @override + void dispose() { + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + super.dispose(); + } + + Set get all => Set.unmodifiable(_rows); + + VaultDetails? _detailsForPath(String dirPath) => _rows.firstWhereOrNull((v) => v.path == dirPath); + + Future create(VaultDetails details) async { + await metadataDb.addVaults({details}); + + _rows.add(details); + _vaultDirPaths = null; + _unlockedDirPaths.add(details.path); + _onLockStateChanged(); + } + + Future remove(Set dirPaths) async { + final details = dirPaths.map(_detailsForPath).whereNotNull().toSet(); + if (details.isEmpty) return; + + await metadataDb.removeVaults(details); + + await Future.forEach(details, (v) => securityService.writeValue(v.passKey, null)); + + _rows.removeAll(details); + _vaultDirPaths = null; + _unlockedDirPaths.removeAll(dirPaths); + _onLockStateChanged(); + } + + Future rename(String oldDirPath, String newDirPath) async { + final oldDetails = _detailsForPath(oldDirPath); + if (oldDetails == null) return; + + final newName = VaultDetails.nameFromPath(newDirPath); + if (newName == null) return; + + final newDetails = oldDetails.copyWith(name: newName); + await metadataDb.updateVault(oldDetails.name, newDetails); + + final pass = await securityService.readValue(oldDetails.passKey); + if (pass != null) { + await securityService.writeValue(newDetails.passKey, pass); + } + + _rows + ..remove(oldDetails) + ..add(newDetails); + _vaultDirPaths = null; + _unlockedDirPaths + ..remove(oldDirPath) + ..add(newDirPath); + _onLockStateChanged(); + } + + // update details, except name + Future update(VaultDetails newDetails) async { + final oldDetails = _detailsForPath(newDetails.path); + if (oldDetails == null) return; + + await metadataDb.updateVault(newDetails.name, newDetails); + + _rows + ..remove(oldDetails) + ..add(newDetails); + } + + Future clear() async { + await metadataDb.clearVaults(); + _rows.clear(); + _vaultDirPaths = null; + } + + Set? _vaultDirPaths; + + Set get vaultDirectories { + _vaultDirPaths ??= _rows.map((v) => v.path).toSet(); + return _vaultDirPaths!; + } + + VaultDetails? getVault(String? dirPath) => all.firstWhereOrNull((v) => v.path == dirPath); + + bool isVault(String dirPath) => vaultDirectories.contains(dirPath); + + bool isLocked(String dirPath) => isVault(dirPath) && !_unlockedDirPaths.contains(dirPath); + + bool isVaultEntryUri(String uriString) { + final uri = Uri.parse(uriString); + if (uri.scheme != 'file') return false; + + final path = uri.pathSegments.fold('', (prev, v) => '$prev${pContext.separator}$v'); + return vaultDirectories.any(path.startsWith); + } + + void lock(Set dirPaths) { + final unlocked = dirPaths.where((v) => isVault(v) && !isLocked(v)).toSet(); + if (unlocked.isEmpty) return; + + _unlockedDirPaths.removeAll(unlocked); + _onLockStateChanged(); + } + + Future tryUnlock(String dirPath, BuildContext context) async { + if (!isVault(dirPath) || !isLocked(dirPath)) return true; + + final details = _detailsForPath(dirPath); + if (details == null) return false; + + bool? confirmed; + switch (details.lockType) { + case VaultLockType.system: + try { + confirmed = await LocalAuthentication().authenticate( + localizedReason: context.l10n.authenticateToUnlockVault, + ); + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + break; + case VaultLockType.pin: + final pin = await showDialog( + context: context, + builder: (context) => const PinDialog(needConfirmation: false), + routeSettings: const RouteSettings(name: PinDialog.routeName), + ); + if (pin != null) { + confirmed = pin == await securityService.readValue(details.passKey); + } + break; + case VaultLockType.password: + final password = await showDialog( + context: context, + builder: (context) => const PasswordDialog(needConfirmation: false), + routeSettings: const RouteSettings(name: PasswordDialog.routeName), + ); + if (password != null) { + confirmed = password == await securityService.readValue(details.passKey); + } + break; + } + + if (confirmed == null || !confirmed) return false; + + _unlockedDirPaths.add(dirPath); + _onLockStateChanged(); + return true; + } + + Future setPass(BuildContext context, VaultDetails details) async { + switch (details.lockType) { + case VaultLockType.system: + final l10n = context.l10n; + try { + return await LocalAuthentication().authenticate( + localizedReason: l10n.authenticateToConfigureVault, + ); + } on PlatformException catch (e, stack) { + await showDialog( + context: context, + builder: (context) => AvesDialog( + content: Text(e.message ?? l10n.genericFailureFeedback), + actions: const [OkButton()], + ), + routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), + ); + if (e.code != auth_error.notAvailable) { + await reportService.recordError(e, stack); + } + } + break; + case VaultLockType.pin: + final pin = await showDialog( + context: context, + builder: (context) => const PinDialog(needConfirmation: true), + routeSettings: const RouteSettings(name: PinDialog.routeName), + ); + if (pin != null) { + return await securityService.writeValue(details.passKey, pin); + } + break; + case VaultLockType.password: + final password = await showDialog( + context: context, + builder: (context) => const PasswordDialog(needConfirmation: true), + routeSettings: const RouteSettings(name: PasswordDialog.routeName), + ); + if (password != null) { + return await securityService.writeValue(details.passKey, password); + } + break; + } + return false; + } + + void _onScreenOff() => lock(all.where((v) => v.autoLockScreenOff).map((v) => v.path).toSet()); + + void _onLockStateChanged() { + windowService.secureScreen(_unlockedDirPaths.isNotEmpty); + notifyListeners(); + } +} diff --git a/lib/ref/mime_types.dart b/lib/ref/mime_types.dart index 744fc9712..4a993fe42 100644 --- a/lib/ref/mime_types.dart +++ b/lib/ref/mime_types.dart @@ -55,6 +55,7 @@ class MimeTypes { static const aviMSVideo = 'video/msvideo'; static const aviVnd = 'video/vnd.avi'; static const aviXMSVideo = 'video/x-msvideo'; + static const dvd = 'video/dvd'; static const flv = 'video/flv'; static const flvX = 'video/x-flv'; static const mkv = 'video/mkv'; @@ -90,7 +91,7 @@ class MimeTypes { static const Set _knownOpaqueImages = {jpeg}; - static const Set _knownVideos = {v3gpp, asf, avi, aviMSVideo, aviVnd, aviXMSVideo, flv, flvX, mkv, mkvX, mov, movX, mp2p, mp2t, mp2ts, mp4, mpeg, ogv, realVideo, webm, wmv}; + static const Set _knownVideos = {v3gpp, asf, avi, aviMSVideo, aviVnd, aviXMSVideo, dvd, flv, flvX, mkv, mkvX, mov, movX, mp2p, mp2t, mp2ts, mp4, mpeg, ogv, realVideo, webm, wmv}; static final Set knownMediaTypes = { anyImage, diff --git a/lib/services/analysis_service.dart b/lib/services/analysis_service.dart index 22389e6dd..31a0c26e2 100644 --- a/lib/services/analysis_service.dart +++ b/lib/services/analysis_service.dart @@ -29,6 +29,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', { 'entryIds': entryIds, diff --git a/lib/services/common/services.dart b/lib/services/common/services.dart index 6962bd6c5..6fadb62e1 100644 --- a/lib/services/common/services.dart +++ b/lib/services/common/services.dart @@ -12,6 +12,7 @@ import 'package:aves/services/media/media_session_service.dart'; import 'package:aves/services/media/media_store_service.dart'; import 'package:aves/services/metadata/metadata_edit_service.dart'; import 'package:aves/services/metadata/metadata_fetch_service.dart'; +import 'package:aves/services/security_service.dart'; import 'package:aves/services/storage_service.dart'; import 'package:aves/services/window_service.dart'; import 'package:aves_report/aves_report.dart'; @@ -41,6 +42,7 @@ final MetadataEditService metadataEditService = getIt(); final MetadataFetchService metadataFetchService = getIt(); final MobileServices mobileServices = getIt(); final ReportService reportService = getIt(); +final SecurityService securityService = getIt(); final StorageService storageService = getIt(); final WindowService windowService = getIt(); @@ -60,6 +62,7 @@ void initPlatformServices() { getIt.registerLazySingleton(PlatformMetadataFetchService.new); getIt.registerLazySingleton(PlatformMobileServices.new); getIt.registerLazySingleton(PlatformReportService.new); + getIt.registerLazySingleton(PlatformSecurityService.new); getIt.registerLazySingleton(PlatformStorageService.new); getIt.registerLazySingleton(PlatformWindowService.new); } diff --git a/lib/services/device_service.dart b/lib/services/device_service.dart index b6d0612cb..03ea12764 100644 --- a/lib/services/device_service.dart +++ b/lib/services/device_service.dart @@ -8,7 +8,7 @@ abstract class DeviceService { Future> getCapabilities(); - Future getDefaultTimeZone(); + Future getDefaultTimeZoneRawOffsetMillis(); Future> getLocales(); @@ -45,9 +45,9 @@ class PlatformDeviceService implements DeviceService { } @override - Future getDefaultTimeZone() async { + Future getDefaultTimeZoneRawOffsetMillis() async { try { - return await _platform.invokeMethod('getDefaultTimeZone'); + return await _platform.invokeMethod('getDefaultTimeZoneRawOffsetMillis'); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } diff --git a/lib/services/media/media_session_service.dart b/lib/services/media/media_session_service.dart index c6cb94b8a..b453716e0 100644 --- a/lib/services/media/media_session_service.dart +++ b/lib/services/media/media_session_service.dart @@ -6,6 +6,7 @@ import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:get_it/get_it.dart'; abstract class MediaSessionService { Stream get mediaCommands; @@ -15,14 +16,22 @@ abstract class MediaSessionService { Future release(); } -class PlatformMediaSessionService implements MediaSessionService { +class PlatformMediaSessionService implements MediaSessionService, Disposable { static const _platformObject = MethodChannel('deckers.thibault/aves/media_session'); + final List _subscriptions = []; final EventChannel _mediaCommandChannel = const OptionalEventChannel('deckers.thibault/aves/media_command'); final StreamController _streamController = StreamController.broadcast(); PlatformMediaSessionService() { - _mediaCommandChannel.receiveBroadcastStream().listen((event) => _onMediaCommand(event as Map?)); + _subscriptions.add(_mediaCommandChannel.receiveBroadcastStream().listen((event) => _onMediaCommand(event as Map?))); + } + + @override + FutureOr onDispose() { + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); } @override diff --git a/lib/services/metadata/metadata_edit_service.dart b/lib/services/metadata/metadata_edit_service.dart index aeb8aec66..8b7fa8c28 100644 --- a/lib/services/metadata/metadata_edit_service.dart +++ b/lib/services/metadata/metadata_edit_service.dart @@ -36,9 +36,7 @@ class PlatformMetadataEditService implements MetadataEditService { }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { - await reportService.recordError(e, stack); - } + await _processPlatformException(entry, e, stack); } return {}; } @@ -52,9 +50,7 @@ class PlatformMetadataEditService implements MetadataEditService { }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { - await reportService.recordError(e, stack); - } + await _processPlatformException(entry, e, stack); } return {}; } @@ -70,9 +66,7 @@ class PlatformMetadataEditService implements MetadataEditService { }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { - await reportService.recordError(e, stack); - } + await _processPlatformException(entry, e, stack); } return {}; } @@ -91,9 +85,7 @@ class PlatformMetadataEditService implements MetadataEditService { }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { - await reportService.recordError(e, stack); - } + await _processPlatformException(entry, e, stack); } return {}; } @@ -106,9 +98,7 @@ class PlatformMetadataEditService implements MetadataEditService { }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { - await reportService.recordError(e, stack); - } + await _processPlatformException(entry, e, stack); } return {}; } @@ -122,10 +112,52 @@ class PlatformMetadataEditService implements MetadataEditService { }); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { - if (!entry.isMissingAtPath) { - await reportService.recordError(e, stack); - } + await _processPlatformException(entry, e, stack); } return {}; } + + Future _processPlatformException(AvesEntry entry, PlatformException e, StackTrace stack) async { + if (!entry.isMissingAtPath) { + final code = e.code; + if (code.endsWith('mp4largemoov')) { + await reportService.recordError(_Mp4LargeMoovException(code: e.code, message: e.message, details: e.details, stacktrace: e.stacktrace), stack); + } else if (code.endsWith('mp4largeother')) { + await reportService.recordError(_Mp4LargeOtherException(code: e.code, message: e.message, details: e.details, stacktrace: e.stacktrace), stack); + } else if (code.endsWith('filenotfound')) { + await reportService.recordError(_FileNotFoundException(code: e.code, message: e.message, details: e.details, stacktrace: e.stacktrace), stack); + } else { + await reportService.recordError(e, stack); + } + } + } +} + +// distinct exceptions to convince Crashlytics to split reports into distinct issues + +class _Mp4LargeMoovException extends PlatformException { + _Mp4LargeMoovException({ + required super.code, + required super.message, + required super.details, + required super.stacktrace, + }); +} + +class _Mp4LargeOtherException extends PlatformException { + _Mp4LargeOtherException({ + required super.code, + required super.message, + required super.details, + required super.stacktrace, + }); +} + +class _FileNotFoundException extends PlatformException { + _FileNotFoundException({ + required super.code, + required super.message, + required super.details, + required super.stacktrace, + }); } diff --git a/lib/services/metadata/metadata_fetch_service.dart b/lib/services/metadata/metadata_fetch_service.dart index 56907877c..16c52177f 100644 --- a/lib/services/metadata/metadata_fetch_service.dart +++ b/lib/services/metadata/metadata_fetch_service.dart @@ -5,6 +5,7 @@ import 'package:aves/model/metadata/fields.dart'; import 'package:aves/model/metadata/overlay.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/model/panorama.dart'; +import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/service_policy.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/metadata/xmp.dart'; @@ -65,6 +66,11 @@ class PlatformMetadataFetchService implements MetadataFetchService { Future getCatalogMetadata(AvesEntry entry, {bool background = false}) async { if (entry.isSvg) return null; + // TODO TLAD remove log when MP4/TIFF-related OOMs are fixed + if ({MimeTypes.mp4, MimeTypes.tiff}.contains(entry.mimeType) && (entry.sizeBytes ?? 0) > 20000000) { + await reportService.log('catalog large entry=$entry size=${entry.sizeBytes}'); + } + Future call() async { try { // returns map with: diff --git a/lib/services/security_service.dart b/lib/services/security_service.dart new file mode 100644 index 000000000..d6753c733 --- /dev/null +++ b/lib/services/security_service.dart @@ -0,0 +1,39 @@ +import 'package:aves/services/common/services.dart'; +import 'package:flutter/services.dart'; + +abstract class SecurityService { + Future writeValue(String key, T? value); + + Future readValue(String key); +} + +class PlatformSecurityService implements SecurityService { + static const _platform = MethodChannel('deckers.thibault/aves/security'); + + @override + Future writeValue(String key, T? value) async { + try { + await _platform.invokeMethod('writeValue', { + 'key': key, + 'value': value, + }); + return true; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return false; + } + + @override + Future readValue(String key) async { + try { + final result = await _platform.invokeMethod('readValue', { + 'key': key, + }); + if (result != null) return result as T; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return null; + } +} diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index a42f36e0a..2718acb32 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -9,6 +9,8 @@ import 'package:streams_channel/streams_channel.dart'; abstract class StorageService { Future> getStorageVolumes(); + Future getVaultRoot(); + Future getFreeSpace(StorageVolume volume); Future> getGrantedDirectories(); @@ -53,6 +55,17 @@ class PlatformStorageService implements StorageService { return {}; } + @override + Future getVaultRoot() async { + try { + final result = await _platform.invokeMethod('getVaultRoot'); + return result as String; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return ''; + } + @override Future getFreeSpace(StorageVolume volume) async { try { diff --git a/lib/services/window_service.dart b/lib/services/window_service.dart index 576b832b8..81796bff8 100644 --- a/lib/services/window_service.dart +++ b/lib/services/window_service.dart @@ -8,6 +8,8 @@ abstract class WindowService { Future keepScreenOn(bool on); + Future secureScreen(bool on); + Future isRotationLocked(); Future requestOrientation([Orientation? orientation]); @@ -42,6 +44,17 @@ class PlatformWindowService implements WindowService { } } + @override + Future secureScreen(bool on) async { + try { + await _platform.invokeMethod('secureScreen', { + 'on': on, + }); + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + } + @override Future isRotationLocked() async { try { diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index cd05e0c89..a85aea75a 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -60,8 +60,8 @@ class AIcons { // view static const IconData group = Icons.group_work_outlined; static const IconData layout = Icons.grid_view_outlined; - static const IconData layoutMosaic = Icons.view_compact_outlined; - static const IconData layoutGrid = Icons.view_comfy_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; @@ -132,6 +132,9 @@ class AIcons { 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 zoomIn = Icons.add_outlined; @@ -147,6 +150,8 @@ class AIcons { 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; // thumbnail overlay static const IconData animated = Icons.slideshow; diff --git a/lib/theme/themes.dart b/lib/theme/themes.dart index 57d931862..c23832fdf 100644 --- a/lib/theme/themes.dart +++ b/lib/theme/themes.dart @@ -50,7 +50,6 @@ class Themes { cardColor: _lightSecondLayer, dialogBackgroundColor: _lightSecondLayer, indicatorColor: accentColor, - toggleableActiveColor: accentColor, typography: _typography, appBarTheme: AppBarTheme( backgroundColor: _lightFirstLayer, @@ -104,7 +103,6 @@ class Themes { cardColor: _darkSecondLayer, dialogBackgroundColor: _darkSecondLayer, indicatorColor: accentColor, - toggleableActiveColor: accentColor, typography: _typography, appBarTheme: AppBarTheme( backgroundColor: _darkFirstLayer, diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index df8162f12..d49784fb6 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:collection/collection.dart'; @@ -10,7 +11,8 @@ final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); class AndroidFileUtils { static const String trashDirPath = '#trash'; - late final String separator, primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath, avesVideoCapturesPath; + late final String separator, vaultRoot, primaryStorage; + late final String dcimPath, downloadPath, moviesPath, picturesPath, avesVideoCapturesPath; late final Set videoCapturesPaths; Set storageVolumes = {}; Set _packages = {}; @@ -28,6 +30,7 @@ class AndroidFileUtils { separator = pContext.separator; await _initStorageVolumes(); + vaultRoot = await storageService.getVaultRoot(); primaryStorage = storageVolumes.firstWhereOrNull((volume) => volume.isPrimary)?.path ?? separator; // standard dcimPath = pContext.join(primaryStorage, 'DCIM'); @@ -90,15 +93,17 @@ class AndroidFileUtils { bool isOnRemovableStorage(String path) => getStorageVolume(path)?.isRemovable ?? false; - AlbumType getAlbumType(String albumPath) { - if (isCameraPath(albumPath)) return AlbumType.camera; - if (isDownloadPath(albumPath)) return AlbumType.download; - if (isScreenRecordingsPath(albumPath)) return AlbumType.screenRecordings; - if (isScreenshotsPath(albumPath)) return AlbumType.screenshots; - if (isVideoCapturesPath(albumPath)) return AlbumType.videoCaptures; + AlbumType getAlbumType(String dirPath) { + if (vaults.isVault(dirPath)) return AlbumType.vault; - final dir = pContext.split(albumPath).last; - if (albumPath.startsWith(primaryStorage) && _potentialAppDirs.contains(dir)) return AlbumType.app; + if (isCameraPath(dirPath)) return AlbumType.camera; + if (isDownloadPath(dirPath)) return AlbumType.download; + if (isScreenRecordingsPath(dirPath)) return AlbumType.screenRecordings; + if (isScreenshotsPath(dirPath)) return AlbumType.screenshots; + if (isVideoCapturesPath(dirPath)) return AlbumType.videoCaptures; + + final dir = pContext.split(dirPath).last; + if (dirPath.startsWith(primaryStorage) && _potentialAppDirs.contains(dir)) return AlbumType.app; return AlbumType.regular; } @@ -115,7 +120,16 @@ class AndroidFileUtils { } } -enum AlbumType { regular, app, camera, download, screenRecordings, screenshots, videoCaptures } +enum AlbumType { + regular, + vault, + app, + camera, + download, + screenRecordings, + screenshots, + videoCaptures, +} class Package { final String packageName; diff --git a/lib/utils/collection_utils.dart b/lib/utils/collection_utils.dart index 4983a8d62..d464e1429 100644 --- a/lib/utils/collection_utils.dart +++ b/lib/utils/collection_utils.dart @@ -17,3 +17,13 @@ extension ExtraMapNullableKeyValue on Map { extension ExtraNumIterable on Iterable { int get sum => fold(0, (prev, v) => prev + (v ?? 0)); } + +extension ExtraEnum on Iterable { + T safeByName(String name, T defaultValue) { + try { + return byName(name); + } catch (error) { + return defaultValue; + } + } +} diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 859ae55f6..c47a3f4db 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -2,8 +2,16 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; +import 'package:permission_handler/permission_handler.dart'; class Constants { + static const storagePermissions = [ + Permission.storage, + // for media access on Android >=13 + Permission.photos, + Permission.videos, + ]; + static const separator = ' • '; // `Color(0x00FFFFFF)` is different from `Color(0x00000000)` (or `Colors.transparent`) diff --git a/lib/utils/dependencies.dart b/lib/utils/dependencies.dart index 27a0c94c9..4076d7df9 100644 --- a/lib/utils/dependencies.dart +++ b/lib/utils/dependencies.dart @@ -80,6 +80,12 @@ class Dependencies { license: mit, sourceUrl: 'https://github.com/ajinasokan/flutter_displaymode', ), + 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', + ), Dependency( name: 'Package Info Plus', license: bsd3, @@ -101,11 +107,17 @@ class Dependencies { license: mit, sourceUrl: 'https://github.com/aaassseee/screen_brightness', ), + Dependency( + name: 'Screen State', + license: mit, + licenseUrl: 'https://github.com/cph-cachet/flutter-plugins/blob/master/packages/screen_state/LICENSE', + sourceUrl: 'https://github.com/cph-cachet/flutter-plugins/tree/master/packages/screen_state', + ), Dependency( name: 'Shared Preferences', license: bsd3, - licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/shared_preferences/shared_preferences/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences', + 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', ), Dependency( name: 'sqflite', @@ -120,8 +132,8 @@ class Dependencies { Dependency( name: 'URL Launcher', license: bsd3, - licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher', + 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', ), Dependency( name: 'Volume Controller', @@ -139,8 +151,8 @@ class Dependencies { Dependency( name: 'Google Maps for Flutter', license: bsd3, - licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter', + 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', ), ]; @@ -219,8 +231,8 @@ class Dependencies { Dependency( name: 'Flutter Markdown', license: bsd3, - licenseUrl: 'https://github.com/flutter/packages/blob/master/packages/flutter_markdown/LICENSE', - sourceUrl: 'https://github.com/flutter/packages/tree/master/packages/flutter_markdown', + licenseUrl: 'https://github.com/flutter/packages/blob/main/packages/flutter_markdown/LICENSE', + sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/flutter_markdown', ), Dependency( name: 'Flutter Staggered Animations', @@ -240,8 +252,8 @@ class Dependencies { Dependency( name: 'Palette Generator', license: bsd3, - licenseUrl: 'https://github.com/flutter/packages/blob/master/packages/palette_generator/LICENSE', - sourceUrl: 'https://github.com/flutter/packages/tree/master/packages/palette_generator', + licenseUrl: 'https://github.com/flutter/packages/blob/main/packages/palette_generator/LICENSE', + sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/palette_generator', ), Dependency( name: 'Panorama (Aves fork)', @@ -253,6 +265,11 @@ class Dependencies { license: bsd2, sourceUrl: 'https://github.com/diegoveloper/flutter_percent_indicator', ), + Dependency( + name: 'Pinput', + license: mit, + sourceUrl: 'https://github.com/Tkko/Flutter_PinPut', + ), Dependency( name: 'Provider', license: mit, @@ -294,8 +311,8 @@ class Dependencies { Dependency( name: 'Flutter Lints', license: bsd3, - licenseUrl: 'https://github.com/flutter/packages/blob/master/packages/flutter_lints/LICENSE', - sourceUrl: 'https://github.com/flutter/packages/tree/master/packages/flutter_lints', + licenseUrl: 'https://github.com/flutter/packages/blob/main/packages/flutter_lints/LICENSE', + sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/flutter_lints', ), Dependency( name: 'Get It', diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart index 4888d01bc..357607629 100644 --- a/lib/widgets/about/about_page.dart +++ b/lib/widgets/about/about_page.dart @@ -5,6 +5,7 @@ import 'package:aves/widgets/about/credits.dart'; import 'package:aves/widgets/about/licenses.dart'; import 'package:aves/widgets/about/translators.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/tv_edge_focus.dart'; import 'package:aves/widgets/common/behaviour/pop/scope.dart'; import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; @@ -50,7 +51,7 @@ class AboutPage extends StatelessWidget { ); if (useTvLayout) { - return Scaffold( + return AvesScaffold( body: AvesPopScope( handlers: const [TvNavigationPopHandler.pop], child: Row( @@ -69,7 +70,7 @@ class AboutPage extends StatelessWidget { ), ); } else { - return Scaffold( + return AvesScaffold( appBar: AppBar( title: appBarTitle, ), diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart index fbab83d89..111b97c6b 100644 --- a/lib/widgets/about/app_ref.dart +++ b/lib/widgets/about/app_ref.dart @@ -102,8 +102,7 @@ class _AppReferenceState extends State { } void _goToPolicyPage() { - Navigator.push( - context, + Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: PolicyPage.routeName), builder: (context) => const PolicyPage(), diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index 50b376ce3..ac1f1b1f1 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -80,8 +80,7 @@ class _LicensesState extends State { Center( child: AvesOutlinedButton( label: context.l10n.aboutLicensesShowAllButtonLabel, - onPressed: () => Navigator.push( - context, + onPressed: () => Navigator.maybeOf(context)?.push( MaterialPageRoute( builder: (context) => Theme( data: Theme.of(context).copyWith( diff --git a/lib/widgets/about/policy_page.dart b/lib/widgets/about/policy_page.dart index 3803bae2f..e5c953087 100644 --- a/lib/widgets/about/policy_page.dart +++ b/lib/widgets/about/policy_page.dart @@ -1,5 +1,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/basic/markdown_container.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; +import 'package:aves/widgets/common/behaviour/intents.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -28,7 +30,7 @@ class _PolicyPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return AvesScaffold( appBar: AppBar( automaticallyImplyLeading: !settings.useTvLayout, title: Text(context.l10n.policyPageTitle), @@ -37,11 +39,11 @@ class _PolicyPageState extends State { child: FocusableActionDetector( autofocus: true, shortcuts: const { - SingleActivator(LogicalKeyboardKey.arrowUp): _ScrollIntent.up(), - SingleActivator(LogicalKeyboardKey.arrowDown): _ScrollIntent.down(), + SingleActivator(LogicalKeyboardKey.arrowUp): VerticalScrollIntent.up(), + SingleActivator(LogicalKeyboardKey.arrowDown): VerticalScrollIntent.down(), }, actions: { - _ScrollIntent: CallbackAction<_ScrollIntent>(onInvoke: _onScrollIntent), + VerticalScrollIntent: VerticalScrollIntentAction(scrollController: _scrollController), }, child: Center( child: FutureBuilder( @@ -64,38 +66,4 @@ class _PolicyPageState extends State { ), ); } - - void _onScrollIntent(_ScrollIntent intent) { - late int factor; - switch (intent.type) { - case _ScrollDirection.up: - factor = -1; - break; - case _ScrollDirection.down: - factor = 1; - break; - } - _scrollController.animateTo( - _scrollController.offset + factor * 150, - duration: const Duration(milliseconds: 500), - curve: Curves.easeOutCubic, - ); - } -} - -class _ScrollIntent extends Intent { - const _ScrollIntent({ - required this.type, - }); - - const _ScrollIntent.up() : type = _ScrollDirection.up; - - const _ScrollIntent.down() : type = _ScrollDirection.down; - - final _ScrollDirection type; -} - -enum _ScrollDirection { - up, - down, } diff --git a/lib/widgets/about/translators.dart b/lib/widgets/about/translators.dart index 7d172bbaa..d1f019f24 100644 --- a/lib/widgets/about/translators.dart +++ b/lib/widgets/about/translators.dart @@ -45,6 +45,8 @@ class AboutTranslators extends StatelessWidget { Contributor('Tijolinho', 'pedrohenrique29.alfenas@gmail.com'), Contributor('Piotr K', '1337.kelt@gmail.com'), Contributor('rehork', 'cooky@e.email'), + Contributor('Eric', 'hamburger2048@users.noreply.hosted.weblate.org'), + Contributor('Aitor Salaberria', 'trslbrr@gmail.com'), // Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'), // Arabic // Contributor('Salih Ail', 'rrrfff444@gmail.com'), // Arabic // Contributor('امیر جهانگرد', 'ijahangard.a@gmail.com'), // Persian @@ -52,6 +54,7 @@ class AboutTranslators extends StatelessWidget { // 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 }; @override diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 8bcdf131b..c9f6d7c0a 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -29,6 +29,7 @@ import 'package:aves/utils/constants.dart'; import 'package:aves/utils/debouncer.dart'; import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/behaviour/route_tracker.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -54,7 +55,7 @@ class AvesApp extends StatefulWidget { final AppFlavor flavor; // temporary exclude locales not ready yet for prime time - static final _unsupportedLocales = {'ar', 'fa', 'gl', 'he', 'nn', 'th'}.map(Locale.new).toSet(); + static final _unsupportedLocales = {'ar', 'fa', 'gl', 'he', 'nn', 'sk', 'th'}.map(Locale.new).toSet(); static final List supportedLocales = AppLocalizations.supportedLocales.where((v) => !_unsupportedLocales.contains(v)).toList(); static final ValueNotifier cutoutInsetsNotifier = ValueNotifier(EdgeInsets.zero); static final GlobalKey navigatorKey = GlobalKey(debugLabel: 'app-navigator'); @@ -210,7 +211,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { } final home = initialized ? _getFirstPage() - : Scaffold( + : AvesScaffold( body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(), ); return Selector>( @@ -353,6 +354,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { debugPrint('$runtimeType lifecycle ${state.name}'); + reportService.log('Lifecycle ${state.name}'); switch (state) { case AppLifecycleState.inactive: switch (_appModeNotifier.value) { @@ -556,7 +558,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { // do not reset when relaunching the app if (_appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) return; - reportService.log('New intent'); + reportService.log('New intent data=$intentData'); AvesApp.navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute( settings: const RouteSettings(name: HomePage.routeName), builder: (_) => _getFirstPage(intentData: intentData), diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 3c697f6eb..6991d4ed9 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -666,6 +666,7 @@ class _CollectionAppBarState extends State with SingleTickerPr tileExtentController: extentController, ); }, + routeSettings: const RouteSettings(name: TileViewDialog.routeName), ); // wait for the dialog to hide as applying the change may block the UI await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); @@ -678,8 +679,7 @@ class _CollectionAppBarState extends State with SingleTickerPr } void _goToSearch() { - Navigator.push( - context, + Navigator.maybeOf(context)?.push( SearchPageRoute( delegate: CollectionSearchDelegate( searchFieldLabel: context.l10n.searchCollectionFieldHint, diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index a063ed908..bf0c69973 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -13,6 +13,7 @@ import 'package:aves/model/source/section_keys.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/collection/app_bar.dart'; import 'package:aves/widgets/collection/draggable_thumb_label.dart'; import 'package:aves/widgets/collection/grid/list_details_theme.dart'; @@ -217,7 +218,7 @@ class _CollectionGridContentState extends State<_CollectionGridContent> { child: _CollectionSectionedContent( collection: collection, isScrollingNotifier: _isScrollingNotifier, - scrollController: PrimaryScrollController.of(context)!, + scrollController: PrimaryScrollController.of(context), tileLayout: tileLayout, selectable: selectable, ), @@ -440,7 +441,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge @override Widget build(BuildContext context) { final scrollView = _buildScrollView(widget.appBar, widget.collection); - return _buildDraggableScrollView(scrollView, widget.collection); + return settings.useTvLayout ? scrollView : _buildDraggableScrollView(scrollView, widget.collection); } Widget _buildDraggableScrollView(ScrollView scrollView, CollectionLens collection) { @@ -642,5 +643,5 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge return crumbs; } - Future get _isStoragePermissionGranted => Permission.storage.status.then((status) => status.isGranted); + Future get _isStoragePermissionGranted => Future.wait(Constants.storagePermissions.map((v) => v.status)).then((v) => v.any((status) => status.isGranted)); } diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 39b2ea928..37d8cdb59 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -7,6 +7,7 @@ import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/highlight.dart'; import 'package:aves/model/selection.dart'; +import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -15,6 +16,7 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/behaviour/pop/double_back.dart'; import 'package:aves/widgets/common/behaviour/pop/scope.dart'; import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; @@ -124,7 +126,7 @@ class _CollectionPageState extends State { Widget page; if (useTvLayout) { - page = Scaffold( + page = AvesScaffold( body: Row( children: [ TvRail( @@ -149,7 +151,7 @@ class _CollectionPageState extends State { _draggableScrollBarEventStreamController.add(notification.event); return false; }, - child: Scaffold( + child: AvesScaffold( body: body, floatingActionButton: _buildFab(context, hasSelection), drawer: canNavigate ? AppDrawer(currentCollection: _collection) : null, @@ -214,11 +216,13 @@ class _CollectionPageState extends State { final highlightTest = widget.highlightTest; if (highlightTest == null) return; + final item = _collection.sortedEntries.firstWhereOrNull(highlightTest); + if (item == null) return; + final delayDuration = context.read().staggeredAnimationPageTarget; await Future.delayed(delayDuration + Durations.highlightScrollInitDelay); - final targetEntry = _collection.sortedEntries.firstWhereOrNull(highlightTest); - if (targetEntry != null) { - context.read().trackItem(targetEntry, highlightItem: targetEntry); - } + + final animate = context.read().accessibilityAnimations.animate; + context.read().trackItem(item, animate: animate, highlightItem: item); } } diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index e23286a9b..1fe1ad605 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -17,6 +17,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/services.dart'; @@ -24,6 +25,7 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/mime_utils.dart'; import 'package:aves/widgets/common/action_mixins/entry_editor.dart'; +import 'package:aves/widgets/common/action_mixins/entry_storage.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; @@ -45,8 +47,6 @@ import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; -import '../common/action_mixins/entry_storage.dart'; - class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin, EntryEditorMixin, EntryStorageMixin { bool isVisible( EntrySetAction action, { @@ -160,6 +160,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware } void onActionSelected(BuildContext context, EntrySetAction action) { + reportService.log('$action'); switch (action) { // general case EntrySetAction.configureView: @@ -243,9 +244,8 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware } } - void _leaveSelectionMode(BuildContext context) { - final selection = context.read?>(); - selection?.browse(); + void _browse(BuildContext context) { + context.read?>()?.browse(); } Set _getTargetItems(BuildContext context) { @@ -267,6 +267,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware content: Text(context.l10n.tooManyItemsErrorDialogMessage), actions: const [OkButton()], ), + routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), ); } } @@ -278,15 +279,34 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware final collection = context.read(); collection.source.analyze(controller, entries: entries); - _leaveSelectionMode(context); + _browse(context); } Future _delete(BuildContext context) async { final entries = _getTargetItems(context); + final byBinUsage = groupBy(entries, (entry) { + final details = vaults.getVault(entry.directory); + return details?.useBin ?? settings.enableBin; + }); + await Future.forEach( + byBinUsage.entries, + (kv) => doDelete( + context: context, + entries: kv.value.toSet(), + enableBin: kv.key, + )); + _browse(context); + } + + Future doDelete({ + required BuildContext context, + required Set entries, + required bool enableBin, + }) async { final pureTrash = entries.every((entry) => entry.trashed); - if (settings.enableBin && !pureTrash) { - await _move(context, moveType: MoveType.toBin); + if (enableBin && !pureTrash) { + await doMove(context, moveType: MoveType.toBin, entries: entries); return; } @@ -295,7 +315,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware final storageDirs = entries.map((e) => e.storageDirectory).whereNotNull().toSet(); final todoCount = entries.length; - if (!await showConfirmationDialog( + if (!await showSkippableConfirmationDialog( context: context, type: ConfirmationDialog.deleteForever, message: l10n.deleteEntriesConfirmationDialogMessage(todoCount), @@ -328,22 +348,19 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware await storageService.deleteEmptyDirectories(storageDirs); }, ); - - _leaveSelectionMode(context); } Future _move(BuildContext context, {required MoveType moveType}) async { final entries = _getTargetItems(context); await doMove(context, moveType: moveType, entries: entries); - _leaveSelectionMode(context); + _browse(context); } Future _rename(BuildContext context) async { final entries = _getTargetItems(context).toList(); - final pattern = await Navigator.push( - context, + final pattern = await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: RenameEntrySetPage.routeName), builder: (context) => RenameEntrySetPage( @@ -359,7 +376,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware })).whereNotNullValue(); await rename(context, entriesToNewName: entriesToNewName, persist: true); - _leaveSelectionMode(context); + _browse(context); } Future _toggleFavourite(BuildContext context) async { @@ -370,7 +387,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware await favourites.add(entries); } - _leaveSelectionMode(context); + _browse(context); } Future _edit( @@ -438,7 +455,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware } }, ); - _leaveSelectionMode(context); + _browse(context); } Future?> _getEditableTargetItems( @@ -468,11 +485,12 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware const CancelButton(), if (supported.isNotEmpty) TextButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () => Navigator.maybeOf(context)?.pop(true), child: Text(l10n.continueButtonLabel), ), ], ), + routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), ); if (confirmed == null || !confirmed) return null; @@ -523,8 +541,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware final editableEntries = await _getEditableItems(context, entries, canEdit: (entry) => entry.canEditLocation); if (editableEntries == null || editableEntries.isEmpty) return null; - final location = await Navigator.push( - context, + final location = await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: LocationPickPage.routeName), builder: (context) => LocationPickPage( @@ -548,11 +565,12 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware actions: [ const CancelButton(), TextButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () => Navigator.maybeOf(context)?.pop(true), child: Text(context.l10n.applyButtonLabel), ), ], ), + routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), ); if (confirmed == null || !confirmed) return; @@ -621,8 +639,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware filters: collection.filters, fixedSelection: entries.where((entry) => entry.hasGps).toList(), ); - await Navigator.push( - context, + await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: MapPage.routeName), builder: (context) => MapPage(collection: mapCollection), @@ -635,8 +652,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware final collection = context.read(); final entries = _getTargetItems(context); - Navigator.push( - context, + Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: SlideshowPage.routeName), builder: (context) { @@ -656,8 +672,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware final collection = context.read(); final entries = _getTargetItems(context); - Navigator.push( - context, + Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: StatsPage.routeName), builder: (context) => StatsPage( @@ -672,8 +687,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware void _goToSearch(BuildContext context) { final collection = context.read(); - Navigator.push( - context, + Navigator.maybeOf(context)?.push( SearchPageRoute( delegate: CollectionSearchDelegate( searchFieldLabel: context.l10n.searchCollectionFieldHint, @@ -701,6 +715,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware defaultName: defaultName ?? '', collection: collection, ), + routeSettings: const RouteSettings(name: AddShortcutDialog.routeName), ); if (result == null) return; diff --git a/lib/widgets/collection/grid/headers/any.dart b/lib/widgets/collection/grid/headers/any.dart index 645c33b2e..c3949add9 100644 --- a/lib/widgets/collection/grid/headers/any.dart +++ b/lib/widgets/collection/grid/headers/any.dart @@ -33,7 +33,7 @@ class CollectionSectionHeader extends StatelessWidget { height: height, child: header, ) - : const SizedBox.shrink(); + : const SizedBox(); } Widget? _buildHeader(BuildContext context) { diff --git a/lib/widgets/collection/grid/tile.dart b/lib/widgets/collection/grid/tile.dart index c37693b3b..9f18792cb 100644 --- a/lib/widgets/collection/grid/tile.dart +++ b/lib/widgets/collection/grid/tile.dart @@ -51,7 +51,7 @@ class InteractiveTile extends StatelessWidget { selection.toggleSelection(entry); break; case AppMode.pickMediaInternal: - Navigator.pop(context, entry); + Navigator.maybeOf(context)?.pop(entry); break; case AppMode.pickCollectionFiltersExternal: case AppMode.pickFilterInternal: @@ -81,8 +81,7 @@ class InteractiveTile extends StatelessWidget { } void _goToViewer(BuildContext context) { - Navigator.push( - context, + Navigator.maybeOf(context)?.push( TransparentMaterialPageRoute( settings: const RouteSettings(name: EntryViewerPage.routeName), pageBuilder: (context, a, sa) { diff --git a/lib/widgets/common/action_controls/quick_choosers/common/button.dart b/lib/widgets/common/action_controls/quick_choosers/common/button.dart index 6b6636b0d..4dcd42334 100644 --- a/lib/widgets/common/action_controls/quick_choosers/common/button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/common/button.dart @@ -87,7 +87,7 @@ abstract class ChooserQuickButtonState, U> exten } void _showChooser(LongPressStartDetails details) { - final overlay = Overlay.of(context)!; + final overlay = Overlay.of(context); final triggerBox = context.findRenderObject() as RenderBox; final overlayBox = overlay.context.findRenderObject() as RenderBox; final triggerRect = RelativeRect.fromRect( diff --git a/lib/widgets/common/action_controls/quick_choosers/move_button.dart b/lib/widgets/common/action_controls/quick_choosers/move_button.dart index 9589524f8..c6ea6163d 100644 --- a/lib/widgets/common/action_controls/quick_choosers/move_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/move_button.dart @@ -38,11 +38,12 @@ class _MoveButtonState extends ChooserQuickButtonState { @override Widget buildChooser(Animation animation, PopupMenuPosition chooserPosition) { - final options = settings.recentDestinationAlbums; + final source = context.read(); + final rawAlbums = source.rawAlbums; + final options = settings.recentDestinationAlbums.where(rawAlbums.contains).toList(); final takeCount = MenuQuickChooser.maxOptionCount - options.length; if (takeCount > 0) { - final source = context.read(); - final filters = source.rawAlbums.whereNot(options.contains).map((album) => AlbumFilter(album, null)).toSet(); + final filters = rawAlbums.whereNot(options.contains).map((album) => AlbumFilter(album, null)).toSet(); final allMapEntries = filters.map((filter) => FilterGridItem(filter, source.recentEntry(filter))).toList(); allMapEntries.sort(FilterNavigationPage.compareFiltersByDate); options.addAll(allMapEntries.take(takeCount).map((v) => v.filter.album)); diff --git a/lib/widgets/common/action_mixins/entry_editor.dart b/lib/widgets/common/action_mixins/entry_editor.dart index 49c411271..40471bbde 100644 --- a/lib/widgets/common/action_mixins/entry_editor.dart +++ b/lib/widgets/common/action_mixins/entry_editor.dart @@ -30,6 +30,7 @@ mixin EntryEditorMixin { entry: entries.first, collection: collection, ), + routeSettings: const RouteSettings(name: EditEntryDateDialog.routeName), ); } @@ -44,6 +45,7 @@ mixin EntryEditorMixin { entry: entry, collection: collection, ), + routeSettings: const RouteSettings(name: EditEntryLocationDialog.routeName), ); } @@ -60,6 +62,7 @@ mixin EntryEditorMixin { initialTitle: initialTitle, initialDescription: initialDescription, ), + routeSettings: const RouteSettings(name: EditEntryTitleDescriptionDialog.routeName), ); } @@ -71,6 +74,7 @@ mixin EntryEditorMixin { builder: (context) => EditEntryRatingDialog( entry: entries.first, ), + routeSettings: const RouteSettings(name: EditEntryRatingDialog.routeName), ); } @@ -82,8 +86,7 @@ mixin EntryEditorMixin { final filters = {...v.tags.map(TagFilter.new)}; return MapEntry(v, filters); })); - await Navigator.push( - context, + await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: TagEditorPage.routeName), builder: (context) => TagEditorPage( @@ -117,6 +120,7 @@ mixin EntryEditorMixin { builder: (context) => RemoveEntryMetadataDialog( showJpegTypes: entries.any((entry) => entry.mimeType == MimeTypes.jpeg), ), + routeSettings: const RouteSettings(name: RemoveEntryMetadataDialog.routeName), ); if (types == null || types.isEmpty) return null; @@ -128,11 +132,12 @@ mixin EntryEditorMixin { actions: [ const CancelButton(), TextButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () => Navigator.maybeOf(context)?.pop(true), child: Text(context.l10n.applyButtonLabel), ), ], ), + routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), ); if (confirmed == null || !confirmed) return null; } diff --git a/lib/widgets/common/action_mixins/entry_storage.dart b/lib/widgets/common/action_mixins/entry_storage.dart index defc70ecf..1d31c5add 100644 --- a/lib/widgets/common/action_mixins/entry_storage.dart +++ b/lib/widgets/common/action_mixins/entry_storage.dart @@ -80,6 +80,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { message: originAlbums.length == 1 ? l10n.nameConflictDialogSingleSourceMessage : l10n.nameConflictDialogMultipleSourceMessage, confirmationButtonLabel: l10n.continueButtonLabel, ), + routeSettings: const RouteSettings(name: AvesSelectionDialog.routeName), ); if (value == null) return; nameConflictStrategy = value; @@ -193,7 +194,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { }) async { if (moveType == MoveType.toBin) { final l10n = context.l10n; - if (!await showConfirmationDialog( + if (!await showSkippableConfirmationDialog( context: context, type: ConfirmationDialog.moveToBin, message: l10n.binEntriesConfirmationDialogMessage(entries.length), @@ -230,6 +231,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { context, moveType: moveType, entriesByDestination: entriesByDestination, + onSuccess: onSuccess, ); } @@ -289,7 +291,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { return dateMillis == null || dateMillis == 0; }).toSet(); if (undatedItems.isNotEmpty) { - if (!await showConfirmationDialog( + if (!await showSkippableConfirmationDialog( context: context, type: ConfirmationDialog.moveUndatedItems, delegate: MoveUndatedConfirmationDialogDelegate(), @@ -330,8 +332,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { targetFilters.removeWhere((f) => f is AlbumFilter); targetFilters.add(AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))); } - unawaited(Navigator.pushAndRemoveUntil( - context, + unawaited(Navigator.maybeOf(context)?.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index 06352fa9d..0fe88c8da 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -135,24 +135,31 @@ mixin FeedbackMixin { required Stream opStream, int? itemCount, VoidCallback? onCancel, - void Function(Set processed)? onDone, - }) => - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => ReportOverlay( - opStream: opStream, - itemCount: itemCount, - onCancel: onCancel, - onDone: (processed) { - Navigator.pop(context); - onDone?.call(processed); - }, - ), - ); + Future Function(Set processed)? onDone, + }) async { + final completer = Completer(); + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => ReportOverlay( + opStream: opStream, + itemCount: itemCount, + onCancel: onCancel, + onDone: (processed) async { + Navigator.maybeOf(context)?.pop(); + await onDone?.call(processed); + completer.complete(); + }, + ), + routeSettings: const RouteSettings(name: ReportOverlay.routeName), + ); + return completer.future; + } } class ReportOverlay extends StatefulWidget { + static const routeName = '/dialog/report_overlay'; + final Stream opStream; final int? itemCount; final VoidCallback? onCancel; diff --git a/lib/widgets/common/action_mixins/permission_aware.dart b/lib/widgets/common/action_mixins/permission_aware.dart index b54f2411b..db158802b 100644 --- a/lib/widgets/common/action_mixins/permission_aware.dart +++ b/lib/widgets/common/action_mixins/permission_aware.dart @@ -56,12 +56,14 @@ mixin PermissionAwareMixin { actions: [ const CancelButton(), TextButton( - onPressed: () => Navigator.pop(context, true), - child: Text(MaterialLocalizations.of(context).okButtonLabel), + onPressed: () => Navigator.maybeOf(context)?.pop(true), + // MD2 button labels were upper case but they are lower case in MD3 + child: Text(MaterialLocalizations.of(context).okButtonLabel.toUpperCase()), ), ], ); }, + routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), ); // abort if the user cancels in Flutter if (confirmed == null || !confirmed) return false; @@ -73,6 +75,7 @@ mixin PermissionAwareMixin { content: Text(context.l10n.missingSystemFilePickerDialogMessage), actions: const [OkButton()], ), + routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), ); return false; } @@ -96,6 +99,7 @@ mixin PermissionAwareMixin { actions: const [OkButton()], ); }, + routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), ); } } diff --git a/lib/widgets/common/action_mixins/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart index 5e57bb021..cfcb39df0 100644 --- a/lib/widgets/common/action_mixins/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -88,6 +88,7 @@ mixin SizeAwareMixin { actions: const [OkButton()], ); }, + routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), ); } } diff --git a/lib/widgets/common/action_mixins/vault_aware.dart b/lib/widgets/common/action_mixins/vault_aware.dart new file mode 100644 index 000000000..5c6e8e55c --- /dev/null +++ b/lib/widgets/common/action_mixins/vault_aware.dart @@ -0,0 +1,32 @@ +import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/vaults/vaults.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/material.dart'; + +mixin VaultAwareMixin on FeedbackMixin { + Future unlockAlbum(BuildContext context, String dirPath) async { + final success = await vaults.tryUnlock(dirPath, context); + if (!success) { + showFeedback(context, context.l10n.genericFailureFeedback); + } + return success; + } + + Future unlockFilter(BuildContext context, CollectionFilter filter) { + return filter is AlbumFilter ? unlockAlbum(context, filter.album) : Future.value(true); + } + + Future unlockFilters(BuildContext context, Set filters) async { + var unlocked = true; + await Future.forEach(filters, (filter) async { + if (unlocked) { + unlocked = await unlockFilter(context, filter); + } + }); + return unlocked; + } + + void lockFilters(Set filters) => vaults.lock(filters.map((v) => v.album).toSet()); +} diff --git a/lib/widgets/common/basic/color_list_tile.dart b/lib/widgets/common/basic/color_list_tile.dart index ae44a47fb..b33196a67 100644 --- a/lib/widgets/common/basic/color_list_tile.dart +++ b/lib/widgets/common/basic/color_list_tile.dart @@ -40,6 +40,7 @@ class ColorListTile extends StatelessWidget { builder: (context) => ColorPickerDialog( initialValue: value, ), + routeSettings: const RouteSettings(name: ColorPickerDialog.routeName), ); if (color != null) { onChanged(color); @@ -50,6 +51,8 @@ class ColorListTile extends StatelessWidget { } class ColorPickerDialog extends StatefulWidget { + static const routeName = '/dialog/pick_color'; + final Color initialValue; const ColorPickerDialog({ @@ -96,7 +99,7 @@ class _ColorPickerDialogState extends State { actions: [ const CancelButton(), TextButton( - onPressed: () => Navigator.pop(context, color), + onPressed: () => Navigator.maybeOf(context)?.pop(color), child: Text(context.l10n.applyButtonLabel), ), ], diff --git a/lib/widgets/common/basic/markdown_container.dart b/lib/widgets/common/basic/markdown_container.dart index a8174795b..14cd63158 100644 --- a/lib/widgets/common/basic/markdown_container.dart +++ b/lib/widgets/common/basic/markdown_container.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:flutter/material.dart'; @@ -15,10 +16,40 @@ class MarkdownContainer extends StatelessWidget { this.scrollController, }); - static const double maxWidth = 460; + static const double mobileMaxWidth = 460; @override Widget build(BuildContext context) { + final useTvLayout = settings.useTvLayout; + + Widget child = Directionality( + textDirection: textDirection ?? Directionality.of(context), + child: Markdown( + data: data, + selectable: true, + onTapLink: (text, href, title) => AvesApp.launchUrl(href), + controller: scrollController, + shrinkWrap: true, + ), + ); + + if (!useTvLayout) { + child = Theme( + data: Theme.of(context).copyWith( + scrollbarTheme: ScrollbarThemeData( + thumbVisibility: MaterialStateProperty.all(true), + radius: const Radius.circular(16), + crossAxisMargin: 6, + mainAxisMargin: 16, + interactive: true, + ), + ), + child: Scrollbar( + child: child, + ), + ); + } + return Container( margin: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( @@ -26,32 +57,10 @@ class MarkdownContainer extends StatelessWidget { border: Border.all(color: Theme.of(context).dividerColor, width: AvesBorder.curvedBorderWidth), borderRadius: const BorderRadius.all(Radius.circular(16)), ), - constraints: const BoxConstraints(maxWidth: maxWidth), + constraints: BoxConstraints(maxWidth: useTvLayout ? double.infinity : mobileMaxWidth), child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(16)), - child: Theme( - data: Theme.of(context).copyWith( - scrollbarTheme: ScrollbarThemeData( - thumbVisibility: MaterialStateProperty.all(true), - radius: const Radius.circular(16), - crossAxisMargin: 6, - mainAxisMargin: 16, - interactive: true, - ), - ), - child: Scrollbar( - child: Directionality( - textDirection: textDirection ?? Directionality.of(context), - child: Markdown( - data: data, - selectable: true, - onTapLink: (text, href, title) => AvesApp.launchUrl(href), - controller: scrollController, - shrinkWrap: true, - ), - ), - ), - ), + child: child, ), ); } diff --git a/lib/widgets/common/basic/scaffold.dart b/lib/widgets/common/basic/scaffold.dart new file mode 100644 index 000000000..e70c0fdda --- /dev/null +++ b/lib/widgets/common/basic/scaffold.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AvesScaffold extends StatelessWidget { + final PreferredSizeWidget? appBar; + final Widget? body; + final Widget? floatingActionButton; + final Widget? drawer; + final Widget? bottomNavigationBar; + final Color? backgroundColor; + final bool? resizeToAvoidBottomInset; + final bool extendBody; + + const AvesScaffold({ + super.key, + this.appBar, + this.body, + this.floatingActionButton, + this.drawer, + this.bottomNavigationBar, + this.backgroundColor, + this.resizeToAvoidBottomInset, + this.extendBody = false, + }); + + @override + Widget build(BuildContext context) { + // prevent conflict between drawer drag gesture and Android navigation gestures + final drawerEnableOpenDragGesture = context.select((mq) => mq.systemGestureInsets.horizontal == 0); + + return Scaffold( + appBar: appBar, + body: body, + floatingActionButton: floatingActionButton, + drawer: drawer, + bottomNavigationBar: bottomNavigationBar, + backgroundColor: backgroundColor, + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + extendBody: extendBody, + drawerEnableOpenDragGesture: drawerEnableOpenDragGesture, + ); + } +} diff --git a/lib/widgets/common/behaviour/intents.dart b/lib/widgets/common/behaviour/intents.dart new file mode 100644 index 000000000..a01585737 --- /dev/null +++ b/lib/widgets/common/behaviour/intents.dart @@ -0,0 +1,44 @@ +import 'package:flutter/widgets.dart'; + +class VerticalScrollIntent extends Intent { + const VerticalScrollIntent({ + required this.type, + }); + + const VerticalScrollIntent.up() : type = VerticalScrollDirection.up; + + const VerticalScrollIntent.down() : type = VerticalScrollDirection.down; + + final VerticalScrollDirection type; +} + +enum VerticalScrollDirection { + up, + down, +} + +class VerticalScrollIntentAction extends CallbackAction { + VerticalScrollIntentAction({ + required ScrollController scrollController, + }) : super(onInvoke: (intent) => _onScrollIntent(intent, scrollController)); + + static void _onScrollIntent( + VerticalScrollIntent intent, + ScrollController scrollController, + ) { + late int factor; + switch (intent.type) { + case VerticalScrollDirection.up: + factor = -1; + break; + case VerticalScrollDirection.down: + factor = 1; + break; + } + scrollController.animateTo( + scrollController.offset + factor * 150, + duration: const Duration(milliseconds: 500), + curve: Curves.easeOutCubic, + ); + } +} diff --git a/lib/widgets/common/behaviour/pop/tv_navigation.dart b/lib/widgets/common/behaviour/pop/tv_navigation.dart index 97480a9af..67d091e0b 100644 --- a/lib/widgets/common/behaviour/pop/tv_navigation.dart +++ b/lib/widgets/common/behaviour/pop/tv_navigation.dart @@ -16,8 +16,7 @@ class TvNavigationPopHandler { return true; } - Navigator.pushAndRemoveUntil( - context, + Navigator.maybeOf(context)?.pushAndRemoveUntil( _getHomeRoute(), (route) => false, ); diff --git a/lib/widgets/common/behaviour/route_tracker.dart b/lib/widgets/common/behaviour/route_tracker.dart index e86f45559..274e08a74 100644 --- a/lib/widgets/common/behaviour/route_tracker.dart +++ b/lib/widgets/common/behaviour/route_tracker.dart @@ -3,16 +3,16 @@ import 'package:flutter/material.dart'; class ReportingRouteTracker extends NavigatorObserver { @override - void didPush(Route route, Route? previousRoute) => reportService.log('Nav didPush to ${_name(route)}'); + void didPush(Route route, Route? previousRoute) => reportService.log('Nav push to ${_name(route)}'); @override - void didPop(Route route, Route? previousRoute) => reportService.log('Nav didPop to ${_name(previousRoute)}'); + void didPop(Route route, Route? previousRoute) => reportService.log('Nav pop to ${_name(previousRoute)}'); @override - void didRemove(Route route, Route? previousRoute) => reportService.log('Nav didRemove to ${_name(previousRoute)}'); + void didRemove(Route route, Route? previousRoute) => reportService.log('Nav remove to ${_name(previousRoute)}'); @override - void didReplace({Route? newRoute, Route? oldRoute}) => reportService.log('Nav didReplace to ${_name(newRoute)}'); + void didReplace({Route? newRoute, Route? oldRoute}) => reportService.log('Nav replace to ${_name(newRoute)}'); String _name(Route? route) => route?.settings.name ?? 'unnamed ${route?.runtimeType}'; } diff --git a/lib/widgets/common/grid/item_tracker.dart b/lib/widgets/common/grid/item_tracker.dart index a693ec0bd..86cf83777 100644 --- a/lib/widgets/common/grid/item_tracker.dart +++ b/lib/widgets/common/grid/item_tracker.dart @@ -110,23 +110,23 @@ class _GridItemTrackerState extends State> with WidgetsBin final itemVisibility = max(0, tileRect.intersect(viewportRect).height) / tileRect.height; if (!event.predicate(itemVisibility)) return; + double scrollOffset = tileRect.top + (tileRect.height - scrollableSize.height) * ((event.alignment.y + 1) / 2); // most of the time the app bar will be scrolled away after scaling, // so we compensate for it to center the focal point thumbnail - final appBarHeight = appBarHeightNotifier.value; - final scrollOffset = appBarHeight + tileRect.top + (tileRect.height - scrollableSize.height) * ((event.alignment.y + 1) / 2); + scrollOffset += appBarHeightNotifier.value; + scrollOffset = scrollOffset.clamp(0, scrollController.position.maxScrollExtent); - if (event.animate) { - if (scrollOffset > 0) { + if (scrollOffset > 0) { + if (event.animate) { await scrollController.animateTo( scrollOffset, duration: Duration(milliseconds: (scrollOffset / 2).round().clamp(Durations.highlightScrollAnimationMinMillis, Durations.highlightScrollAnimationMaxMillis)), curve: Curves.easeInOutCubic, ); + } else { + scrollController.jumpTo(scrollOffset); + await Future.delayed(Durations.highlightJumpDelay); } - } else { - final maxScrollExtent = scrollController.position.maxScrollExtent; - scrollController.jumpTo(scrollOffset.clamp(.0, maxScrollExtent)); - await Future.delayed(Durations.highlightJumpDelay); } final highlightItem = event.highlightItem; diff --git a/lib/widgets/common/grid/scaling.dart b/lib/widgets/common/grid/scaling.dart index 8cf5ec376..ec7c55972 100644 --- a/lib/widgets/common/grid/scaling.dart +++ b/lib/widgets/common/grid/scaling.dart @@ -165,7 +165,7 @@ class _GridScaleGestureDetectorState extends State((v) => v.expansionTileAnimation); final theme = Theme.of(context); - return Theme( - data: theme.copyWith( - colorScheme: theme.colorScheme.copyWith( - // color used by the `ExpansionTileCard` for selected text and icons - secondary: theme.colorScheme.onBackground, - ), - ), - child: ExpansionTileCard( - // key is expected by test driver - key: Key('tilecard-$value'), - value: value, - expandedNotifier: expandedNotifier, - title: titleChild, - expandable: enabled, - initiallyExpanded: initiallyExpanded, - finalPadding: const EdgeInsets.symmetric(vertical: 6.0), - baseColor: theme.scaffoldBackgroundColor, - expandedColor: theme.canvasColor, - duration: animationDuration, - shadowColor: theme.shadowColor, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Divider(thickness: 1, height: 1), - const SizedBox(height: 4), - if (enabled) ...children, - ], - ), + return ExpansionTileCard( + // key is expected by test driver + key: Key('tilecard-$value'), + value: value, + expandedNotifier: expandedNotifier, + title: titleChild, + expandable: enabled, + initiallyExpanded: initiallyExpanded, + finalPadding: const EdgeInsets.symmetric(vertical: 6.0), + baseColor: theme.scaffoldBackgroundColor, + expandedColor: theme.canvasColor, + expandedTextColor: theme.colorScheme.onBackground, + duration: animationDuration, + shadowColor: theme.shadowColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(thickness: 1, height: 1), + const SizedBox(height: 4), + if (enabled) ...children, + ], ), ); } diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index a58f796b8..e23294c20 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -99,14 +99,16 @@ class AvesFilterChip extends StatefulWidget { if (filter is TagFilter) ChipAction.goToTagPage, ChipAction.reverse, ChipAction.hide, + ChipAction.lockVault, ]; // remove focus, if any, to prevent the keyboard from showing up // after the user is done with the popup menu FocusManager.instance.primaryFocus?.unfocus(); - final overlay = Overlay.of(context)!.context.findRenderObject() as RenderBox; + final overlay = Overlay.of(context).context.findRenderObject() as RenderBox; const touchArea = Size(kMinInteractiveDimension, kMinInteractiveDimension); + final actionDelegate = ChipActionDelegate(); final selectedAction = await showMenu( context: context, position: RelativeRect.fromRect(tapPosition & touchArea, Offset.zero & overlay.size), @@ -115,7 +117,7 @@ class AvesFilterChip extends StatefulWidget { child: Text(filter.getLabel(context)), ), const PopupMenuDivider(), - ...actions.map((action) { + ...actions.where((action) => actionDelegate.isVisible(action, filter: filter)).map((action) { late String text; if (action == ChipAction.reverse) { text = filter.reversed ? context.l10n.chipActionFilterIn : context.l10n.chipActionFilterOut; @@ -134,7 +136,7 @@ class AvesFilterChip extends StatefulWidget { if (selectedAction != null) { // wait for the popup menu to hide before proceeding with the action await Future.delayed(Durations.popupMenuAnimation * timeDilation); - ChipActionDelegate().onActionSelected(context, filter, selectedAction); + actionDelegate.onActionSelected(context, filter, selectedAction); } } } diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index 7bb9c9be3..20a3443d9 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -1,6 +1,7 @@ import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/model/covers.dart'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -339,8 +340,9 @@ class IconUtils { height: size, ) : null; + case AlbumType.vault: + return buildIcon(vaults.isLocked(albumPath) ? AIcons.locked : AIcons.unlocked); case AlbumType.regular: - default: return null; } } diff --git a/lib/widgets/common/map/buttons/panel.dart b/lib/widgets/common/map/buttons/panel.dart index e7205b1b5..7e37cb822 100644 --- a/lib/widgets/common/map/buttons/panel.dart +++ b/lib/widgets/common/map/buttons/panel.dart @@ -37,7 +37,7 @@ class MapButtonPanel extends StatelessWidget { if (!settings.useTvLayout) { navigationButton = MapOverlayButton( icon: const BackButtonIcon(), - onPressed: () => Navigator.pop(context), + onPressed: () => Navigator.maybeOf(context)?.pop(), tooltip: MaterialLocalizations.of(context).backButtonTooltip, ); } diff --git a/lib/widgets/common/map/leaflet/map.dart b/lib/widgets/common/map/leaflet/map.dart index d0c60933f..1e3f39ca9 100644 --- a/lib/widgets/common/map/leaflet/map.dart +++ b/lib/widgets/common/map/leaflet/map.dart @@ -203,7 +203,7 @@ class _EntryLeafletMapState extends State> with TickerProv case EntryMapStyle.stamenWatercolor: return const StamenWatercolorLayer(); default: - return const SizedBox.shrink(); + return const SizedBox(); } } diff --git a/lib/widgets/common/search/delegate.dart b/lib/widgets/common/search/delegate.dart index 26dd06c57..9d89bac86 100644 --- a/lib/widgets/common/search/delegate.dart +++ b/lib/widgets/common/search/delegate.dart @@ -61,7 +61,7 @@ abstract class AvesSearchDelegate extends SearchDelegate { void goBack(BuildContext context) { clean(); - Navigator.pop(context); + Navigator.maybeOf(context)?.pop(); } void clean() { diff --git a/lib/widgets/common/search/page.dart b/lib/widgets/common/search/page.dart index 58ebf7e9d..31bfca47d 100644 --- a/lib/widgets/common/search/page.dart +++ b/lib/widgets/common/search/page.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/debouncer.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/behaviour/pop/double_back.dart'; import 'package:aves/widgets/common/behaviour/pop/scope.dart'; import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; @@ -12,11 +13,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; class SearchPage extends StatefulWidget { + static const routeName = '/search'; + final AvesSearchDelegate delegate; final Animation animation; - static const routeName = '/search'; - const SearchPage({ super.key, required this.delegate, @@ -123,7 +124,7 @@ class _SearchPageState extends State { case null: break; } - return Scaffold( + return AvesScaffold( appBar: AppBar( leading: Hero( tag: AvesAppBar.leadingHeroTag, diff --git a/lib/widgets/debug/android_codecs.dart b/lib/widgets/debug/android_codecs.dart index bba35f549..054e4a7d3 100644 --- a/lib/widgets/debug/android_codecs.dart +++ b/lib/widgets/debug/android_codecs.dart @@ -36,7 +36,7 @@ class _DebugAndroidCodecSectionState extends State wit future: _loader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox(); final codecs = snapshot.data!.map((codec) { return codec.map((k, v) => MapEntry(k.toString(), v.toString())); }).toList() diff --git a/lib/widgets/debug/android_dirs.dart b/lib/widgets/debug/android_dirs.dart index fd92c851e..d4abc67ae 100644 --- a/lib/widgets/debug/android_dirs.dart +++ b/lib/widgets/debug/android_dirs.dart @@ -34,7 +34,7 @@ class _DebugAndroidDirSectionState extends State with Au future: _loader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox(); final data = SplayTreeMap.of(snapshot.data!.map((k, v) => MapEntry(k.toString(), v?.toString() ?? 'null'))); return InfoRowGroup(info: data); }, diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 631cd8bd4..63fbd4870 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -9,6 +9,7 @@ import 'package:aves/services/analysis_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/basic/menu.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/behaviour/pop/scope.dart'; import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; @@ -44,7 +45,7 @@ class _AppDebugPageState extends State { Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.ltr, - child: Scaffold( + child: AvesScaffold( appBar: AppBar( title: const Text('Debug'), actions: [ @@ -122,7 +123,7 @@ class _AppDebugPageState extends State { _taskQueueOverlayEntry = OverlayEntry( builder: (context) => const DebugTaskQueueOverlay(), ); - Overlay.of(context)!.insert(_taskQueueOverlayEntry!); + Overlay.of(context).insert(_taskQueueOverlayEntry!); } else { _taskQueueOverlayEntry = null; } @@ -196,8 +197,7 @@ class _AppDebugPageState extends State { ); break; case AppDebugAction.greenScreen: - await Navigator.push( - context, + await Navigator.maybeOf(context)?.push( MaterialPageRoute( builder: (context) => const Scaffold( backgroundColor: Colors.green, diff --git a/lib/widgets/debug/database.dart b/lib/widgets/debug/database.dart index 596eacdcd..dca8795fa 100644 --- a/lib/widgets/debug/database.dart +++ b/lib/widgets/debug/database.dart @@ -4,10 +4,13 @@ import 'package:aves/model/favourites.dart'; import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/trash.dart'; +import 'package:aves/model/vaults/details.dart'; +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/model/video_playback.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; class DebugAppDatabaseSection extends StatefulWidget { @@ -24,6 +27,7 @@ class _DebugAppDatabaseSectionState extends State with late Future> _dbMetadataLoader; late Future> _dbAddressLoader; late Future> _dbTrashLoader; + late Future> _dbVaultsLoader; late Future> _dbFavouritesLoader; late Future> _dbCoversLoader; late Future> _dbVideoPlaybackLoader; @@ -50,7 +54,7 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox(); return Row( children: [ @@ -71,12 +75,14 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox(); + final entries = snapshot.data!; + final byOrigin = groupBy(entries, (entry) => entry.origin); return Row( children: [ Expanded( - child: Text('entry rows: ${snapshot.data!.length}'), + child: Text('entry rows: ${entries.length} (${byOrigin.entries.map((kv) => '${kv.key}: ${kv.value.length}').join(', ')})'), ), const SizedBox(width: 8), ElevatedButton( @@ -92,7 +98,7 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox(); return Row( children: [ @@ -113,7 +119,7 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox(); return Row( children: [ @@ -134,7 +140,7 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox(); return Row( children: [ @@ -155,7 +161,7 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox(); return Row( children: [ @@ -171,12 +177,33 @@ class _DebugAppDatabaseSectionState extends State with ); }, ), + FutureBuilder( + future: _dbVaultsLoader, + builder: (context, snapshot) { + if (snapshot.hasError) return Text(snapshot.error.toString()); + + if (snapshot.connectionState != ConnectionState.done) return const SizedBox(); + + return Row( + children: [ + Expanded( + child: Text('vault rows: ${snapshot.data!.length} (${vaults.all.length} in memory)'), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () => vaults.clear().then((_) => _startDbReport()), + child: const Text('Clear'), + ), + ], + ); + }, + ), FutureBuilder( future: _dbFavouritesLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox(); return Row( children: [ @@ -197,7 +224,7 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox(); return Row( children: [ @@ -218,7 +245,7 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox(); return Row( children: [ @@ -248,6 +275,7 @@ class _DebugAppDatabaseSectionState extends State with _dbMetadataLoader = metadataDb.loadCatalogMetadata(); _dbAddressLoader = metadataDb.loadAddresses(); _dbTrashLoader = metadataDb.loadAllTrashDetails(); + _dbVaultsLoader = metadataDb.loadAllVaults(); _dbFavouritesLoader = metadataDb.loadAllFavourites(); _dbCoversLoader = metadataDb.loadAllCovers(); _dbVideoPlaybackLoader = metadataDb.loadAllVideoPlayback(); diff --git a/lib/widgets/debug/media_store_scan_dialog.dart b/lib/widgets/debug/media_store_scan_dialog.dart index f3c150bdc..e759c29d0 100644 --- a/lib/widgets/debug/media_store_scan_dialog.dart +++ b/lib/widgets/debug/media_store_scan_dialog.dart @@ -41,7 +41,7 @@ class _MediaStoreScanDirDialogState extends State { } }); } - Navigator.pop(context); + Navigator.maybeOf(context)?.pop(); }, child: const Text('Scan'), ) diff --git a/lib/widgets/debug/overlay.dart b/lib/widgets/debug/overlay.dart index b2984037b..6011989a5 100644 --- a/lib/widgets/debug/overlay.dart +++ b/lib/widgets/debug/overlay.dart @@ -18,7 +18,7 @@ class DebugTaskQueueOverlay extends StatelessWidget { child: StreamBuilder( stream: servicePolicy.queueStream, builder: (context, snapshot) { - if (snapshot.hasError) return const SizedBox.shrink(); + if (snapshot.hasError) return const SizedBox(); final queuedEntries = >[]; if (snapshot.hasData) { final state = snapshot.data!; diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart index d4fc6dc75..ee08be83f 100644 --- a/lib/widgets/debug/settings.dart +++ b/lib/widgets/debug/settings.dart @@ -47,6 +47,7 @@ class DebugSettingsSection extends StatelessWidget { padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), child: InfoRowGroup( info: { + 'catalogTimeZoneRawOffsetMillis': '${settings.catalogTimeZoneRawOffsetMillis}', 'tileExtent - Collection': '${settings.getTileExtent(CollectionPage.routeName)}', 'tileExtent - Albums': '${settings.getTileExtent(AlbumListPage.routeName)}', 'tileExtent - Countries': '${settings.getTileExtent(CountryListPage.routeName)}', diff --git a/lib/widgets/dialogs/add_shortcut_dialog.dart b/lib/widgets/dialogs/add_shortcut_dialog.dart index 1d0e8d081..f25fc4108 100644 --- a/lib/widgets/dialogs/add_shortcut_dialog.dart +++ b/lib/widgets/dialogs/add_shortcut_dialog.dart @@ -14,6 +14,8 @@ import 'package:tuple/tuple.dart'; import 'aves_dialog.dart'; class AddShortcutDialog extends StatefulWidget { + static const routeName = '/dialog/add_shortcut'; + final CollectionLens? collection; final String defaultName; @@ -108,8 +110,7 @@ class _AddShortcutDialogState extends State { final _collection = widget.collection; if (_collection == null) return; - final entry = await Navigator.push( - context, + final entry = await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: ItemPickPage.routeName), builder: (context) { @@ -142,7 +143,7 @@ class _AddShortcutDialogState extends State { void _submit(BuildContext context) { if (_isValidNotifier.value) { - Navigator.pop(context, Tuple2(_coverEntry, _nameController.text)); + Navigator.maybeOf(context)?.pop(Tuple2(_coverEntry, _nameController.text)); } } } diff --git a/lib/widgets/dialogs/aves_confirmation_dialog.dart b/lib/widgets/dialogs/aves_confirmation_dialog.dart index 621627172..09cfa1c2d 100644 --- a/lib/widgets/dialogs/aves_confirmation_dialog.dart +++ b/lib/widgets/dialogs/aves_confirmation_dialog.dart @@ -5,6 +5,86 @@ import 'package:flutter/material.dart'; import 'aves_dialog.dart'; +Future showConfirmationDialog({ + required BuildContext context, + required String message, + required String confirmationButtonLabel, +}) async { + final confirmed = await showDialog( + context: context, + builder: (context) => AvesDialog( + content: Text(message), + actions: [ + const CancelButton(), + TextButton( + onPressed: () => Navigator.maybeOf(context)?.pop(true), + child: Text(confirmationButtonLabel), + ), + ], + ), + routeSettings: const RouteSettings(name: AvesDialog.confirmationRouteName), + ); + return confirmed ?? false; +} + +Future showSkippableConfirmationDialog({ + required BuildContext context, + required ConfirmationDialog type, + String? message, + ConfirmationDialogDelegate? delegate, + required String confirmationButtonLabel, +}) async { + if (!_shouldConfirm(type)) return true; + + assert((message != null) ^ (delegate != null)); + final effectiveDelegate = delegate ?? MessageConfirmationDialogDelegate(message!); + final confirmed = await showDialog( + context: context, + builder: (context) => _SkippableConfirmationDialog( + type: type, + delegate: effectiveDelegate, + confirmationButtonLabel: confirmationButtonLabel, + ), + routeSettings: const RouteSettings(name: _SkippableConfirmationDialog.routeName), + ); + if (confirmed == null) return false; + + if (confirmed) { + effectiveDelegate.apply(); + } + return confirmed; +} + +bool _shouldConfirm(ConfirmationDialog type) { + switch (type) { + case ConfirmationDialog.createVault: + return settings.confirmCreateVault; + case ConfirmationDialog.deleteForever: + return settings.confirmDeleteForever; + case ConfirmationDialog.moveToBin: + return settings.confirmMoveToBin; + case ConfirmationDialog.moveUndatedItems: + return settings.confirmMoveUndatedItems; + } +} + +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; + } +} + abstract class ConfirmationDialogDelegate { List build(BuildContext context); @@ -25,74 +105,24 @@ class MessageConfirmationDialogDelegate extends ConfirmationDialogDelegate { ]; } -Future showConfirmationDialog({ - required BuildContext context, - required ConfirmationDialog type, - String? message, - ConfirmationDialogDelegate? delegate, - required String confirmationButtonLabel, -}) async { - if (!_shouldConfirm(type)) return true; +class _SkippableConfirmationDialog extends StatefulWidget { + static const routeName = '/dialog/skippable_confirmation'; - assert((message != null) ^ (delegate != null)); - final effectiveDelegate = delegate ?? MessageConfirmationDialogDelegate(message!); - final confirmed = await showDialog( - context: context, - builder: (context) => _AvesConfirmationDialog( - type: type, - delegate: effectiveDelegate, - confirmationButtonLabel: confirmationButtonLabel, - ), - ); - if (confirmed == null) return false; - - if (confirmed) { - effectiveDelegate.apply(); - } - return confirmed; -} - -bool _shouldConfirm(ConfirmationDialog type) { - switch (type) { - case ConfirmationDialog.deleteForever: - return settings.confirmDeleteForever; - case ConfirmationDialog.moveToBin: - return settings.confirmMoveToBin; - case ConfirmationDialog.moveUndatedItems: - return settings.confirmMoveUndatedItems; - } -} - -void _skipConfirmation(ConfirmationDialog type) { - switch (type) { - case ConfirmationDialog.deleteForever: - settings.confirmDeleteForever = false; - break; - case ConfirmationDialog.moveToBin: - settings.confirmMoveToBin = false; - break; - case ConfirmationDialog.moveUndatedItems: - settings.confirmMoveUndatedItems = false; - break; - } -} - -class _AvesConfirmationDialog extends StatefulWidget { final ConfirmationDialog type; final ConfirmationDialogDelegate delegate; final String confirmationButtonLabel; - const _AvesConfirmationDialog({ + const _SkippableConfirmationDialog({ required this.type, required this.delegate, required this.confirmationButtonLabel, }); @override - State<_AvesConfirmationDialog> createState() => _AvesConfirmationDialogState(); + State<_SkippableConfirmationDialog> createState() => _SkippableConfirmationDialogState(); } -class _AvesConfirmationDialogState extends State<_AvesConfirmationDialog> { +class _SkippableConfirmationDialogState extends State<_SkippableConfirmationDialog> { final ValueNotifier _skip = ValueNotifier(false); @override @@ -116,7 +146,7 @@ class _AvesConfirmationDialogState extends State<_AvesConfirmationDialog> { if (_skip.value) { _skipConfirmation(widget.type); } - Navigator.pop(context, true); + Navigator.maybeOf(context)?.pop(true); }, child: Text(widget.confirmationButtonLabel), ), diff --git a/lib/widgets/dialogs/aves_dialog.dart b/lib/widgets/dialogs/aves_dialog.dart index 2cf3c01e9..c19c0fe5a 100644 --- a/lib/widgets/dialogs/aves_dialog.dart +++ b/lib/widgets/dialogs/aves_dialog.dart @@ -1,13 +1,17 @@ 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'; + static const warningRouteName = '/dialog/warning'; + final String? title; final ScrollController scrollController; final List? scrollableContent; - final bool hasScrollBar; final double horizontalContentPadding; final Widget? content; final List actions; @@ -24,10 +28,9 @@ class AvesDialog extends StatelessWidget { this.title, ScrollController? scrollController, this.scrollableContent, - this.hasScrollBar = true, this.horizontalContentPadding = defaultHorizontalContentPadding, this.content, - required this.actions, + this.actions = const [], }) : assert((scrollableContent != null) ^ (content != null)), scrollController = scrollController ?? ScrollController(); @@ -69,7 +72,7 @@ class AvesDialog extends StatelessWidget { children: scrollableContent!, ); - if (hasScrollBar) { + if (!settings.useTvLayout) { child = Theme( data: Theme.of(context).copyWith( scrollbarTheme: ScrollbarThemeData( @@ -101,7 +104,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: 1, + width: context.select((mq) => mq.size.width / 2), child: DecoratedBox( decoration: contentDecoration(context), child: child, @@ -158,6 +161,7 @@ Future showNoMatchingAppDialog(BuildContext context) => showDialog( content: Text(context.l10n.noMatchingAppDialogMessage), actions: const [OkButton()], ), + routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), ); class CancelButton extends StatelessWidget { @@ -166,8 +170,9 @@ class CancelButton extends StatelessWidget { @override Widget build(BuildContext context) { return TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + onPressed: () => Navigator.maybeOf(context)?.pop(), + // MD2 button labels were upper case but they are lower case in MD3 + child: Text(MaterialLocalizations.of(context).cancelButtonLabel.toUpperCase()), ); } } @@ -178,8 +183,9 @@ class OkButton extends StatelessWidget { @override Widget build(BuildContext context) { return TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).okButtonLabel), + onPressed: () => Navigator.maybeOf(context)?.pop(), + // MD2 button labels were upper case but they are lower case in MD3 + child: Text(MaterialLocalizations.of(context).okButtonLabel.toUpperCase()), ); } } diff --git a/lib/widgets/dialogs/aves_selection_dialog.dart b/lib/widgets/dialogs/aves_selection_dialog.dart index 836d8f48a..bede0ca1c 100644 --- a/lib/widgets/dialogs/aves_selection_dialog.dart +++ b/lib/widgets/dialogs/aves_selection_dialog.dart @@ -13,6 +13,7 @@ Future showSelectionDialog({ final value = await showDialog( context: context, builder: builder, + routeSettings: const RouteSettings(name: AvesSelectionDialog.routeName), ); // wait for the dialog to hide as applying the change may block the UI await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); @@ -24,6 +25,8 @@ Future showSelectionDialog({ typedef TextBuilder = String Function(T value); class AvesSelectionDialog extends StatefulWidget { + static const routeName = '/dialog/selection'; + final T initialValue; final Map options; final TextBuilder? optionSubtitleBuilder; @@ -89,7 +92,7 @@ class _AvesSelectionDialogState extends State> { const CancelButton(), if (needConfirmation) TextButton( - onPressed: () => Navigator.pop(context, _selectedValue), + onPressed: () => Navigator.maybeOf(context)?.pop(_selectedValue), child: Text(confirmationButtonLabel), ), ], @@ -129,7 +132,7 @@ class SelectionRadioListTile extends StatelessWidget { if (needConfirmation) { setGroupValue(v as T); } else { - Navigator.pop(context, v); + Navigator.maybeOf(context)?.pop(v); } }, reselectable: true, diff --git a/lib/widgets/dialogs/duration_dialog.dart b/lib/widgets/dialogs/duration_dialog.dart index 91236ce0a..b6a6b920b 100644 --- a/lib/widgets/dialogs/duration_dialog.dart +++ b/lib/widgets/dialogs/duration_dialog.dart @@ -106,5 +106,5 @@ class _DurationDialogState extends State { ); } - void _submit(BuildContext context) => Navigator.pop(context, _minutes.value * secondsInMinute + _seconds.value); + void _submit(BuildContext context) => Navigator.maybeOf(context)?.pop(_minutes.value * secondsInMinute + _seconds.value); } diff --git a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart index f8c784d10..56bf224cf 100644 --- a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart @@ -21,6 +21,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class EditEntryDateDialog extends StatefulWidget { + static const routeName = '/dialog/edit_entry_date'; + final AvesEntry entry; final CollectionLens? collection; @@ -329,8 +331,7 @@ class _EditEntryDateDialogState extends State { final _collection = widget.collection; if (_collection == null) return; - final entry = await Navigator.push( - context, + final entry = await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: ItemPickPage.routeName), builder: (context) => ItemPickPage( @@ -384,7 +385,7 @@ class _EditEntryDateDialogState extends State { void _submit(BuildContext context) { if (_isValidNotifier.value) { - Navigator.pop(context, _getModifier()); + Navigator.maybeOf(context)?.pop(_getModifier()); } } } diff --git a/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart index a080cc870..4478de495 100644 --- a/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart @@ -6,6 +6,8 @@ import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:flutter/material.dart'; class EditEntryTitleDescriptionDialog extends StatefulWidget { + static const routeName = '/dialog/edit_entry_title_description'; + final String initialTitle, initialDescription; const EditEntryTitleDescriptionDialog({ @@ -103,6 +105,6 @@ class _EditEntryTitleDescriptionDialogState extends State>(context, modifier); + return Navigator.maybeOf(context)?.pop>(modifier); } } diff --git a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart index 31223fd29..b55533e9b 100644 --- a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart @@ -22,6 +22,8 @@ import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; class EditEntryLocationDialog extends StatefulWidget { + static const routeName = '/dialog/edit_entry_location'; + final AvesEntry entry; final CollectionLens? collection; @@ -180,8 +182,7 @@ class _EditEntryLocationDialogState extends State { fixedSelection: baseCollection.sortedEntries.where((entry) => entry.hasGps).toList(), ) : null; - final latLng = await Navigator.push( - context, + final latLng = await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: LocationPickPage.routeName), builder: (context) => LocationPickPage( @@ -222,8 +223,7 @@ class _EditEntryLocationDialogState extends State { final _collection = widget.collection; if (_collection == null) return; - final entry = await Navigator.push( - context, + final entry = await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: ItemPickPage.routeName), builder: (context) => ItemPickPage( @@ -330,16 +330,16 @@ class _EditEntryLocationDialogState extends State { void _submit(BuildContext context) { switch (_action) { case LocationEditAction.chooseOnMap: - Navigator.pop(context, _mapCoordinates); + Navigator.maybeOf(context)?.pop(_mapCoordinates); break; case LocationEditAction.copyItem: - Navigator.pop(context, _copyItemSource.latLng); + Navigator.maybeOf(context)?.pop(_copyItemSource.latLng); break; case LocationEditAction.setCustom: - Navigator.pop(context, _parseLatLng()); + Navigator.maybeOf(context)?.pop(_parseLatLng()); break; case LocationEditAction.remove: - Navigator.pop(context, ExtraAvesEntryMetadataEdition.removalLocation); + Navigator.maybeOf(context)?.pop(ExtraAvesEntryMetadataEdition.removalLocation); break; } } diff --git a/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart index d7b8399a3..e1dab151a 100644 --- a/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart @@ -6,6 +6,8 @@ import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:flutter/material.dart'; class EditEntryRatingDialog extends StatefulWidget { + static const routeName = '/dialog/edit_entry_rating'; + final AvesEntry entry; const EditEntryRatingDialog({ @@ -126,7 +128,7 @@ class _EditEntryRatingDialogState extends State { entryRating = 0; break; } - Navigator.pop(context, entryRating); + Navigator.maybeOf(context)?.pop(entryRating); } } diff --git a/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart b/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart index a2f212008..a2636158d 100644 --- a/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart @@ -9,13 +9,14 @@ import 'package:aves/widgets/common/basic/text/outlined.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/highlight_decoration.dart'; import 'package:aves/widgets/common/identity/highlight_title.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../aves_dialog.dart'; - class RemoveEntryMetadataDialog extends StatefulWidget { + static const routeName = '/dialog/remove_entry_metadata'; + final bool showJpegTypes; const RemoveEntryMetadataDialog({ @@ -136,5 +137,5 @@ class _RemoveEntryMetadataDialogState extends State { void _validate() => _isValidNotifier.value = _types.isNotEmpty; - void _submit(BuildContext context) => Navigator.pop(context, _types); + void _submit(BuildContext context) => Navigator.maybeOf(context)?.pop(_types); } diff --git a/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart b/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart index e99be396d..262b60474 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart @@ -4,11 +4,12 @@ import 'package:aves/model/entry.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:flutter/material.dart'; -import '../aves_dialog.dart'; - class RenameEntryDialog extends StatefulWidget { + static const routeName = '/dialog/rename_entry'; + final AvesEntry entry; const RenameEntryDialog({ @@ -88,7 +89,7 @@ class _RenameEntryDialogState extends State { void _submit(BuildContext context) { if (_isValidNotifier.value) { - Navigator.pop(context, newName); + Navigator.maybeOf(context)?.pop(newName); } } } 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 934bcd1cc..331772cc2 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart @@ -8,6 +8,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/common/basic/menu.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/theme.dart'; import 'package:aves/widgets/common/identity/buttons/outlined_button.dart'; @@ -58,7 +59,7 @@ class _RenameEntrySetPageState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; - return Scaffold( + return AvesScaffold( appBar: AppBar( title: Text(l10n.renameEntrySetPageTitle), ), @@ -185,7 +186,7 @@ class _RenameEntrySetPageState extends State { label: l10n.entryActionRename, onPressed: () { settings.entryRenamingPattern = _patternTextController.text; - Navigator.pop(context, _namingPatternNotifier.value); + Navigator.maybeOf(context)?.pop(_namingPatternNotifier.value); }, ), ), diff --git a/lib/widgets/dialogs/entry_editors/tag_editor_page.dart b/lib/widgets/dialogs/entry_editors/tag_editor_page.dart index 9596b7a41..fe4a06c52 100644 --- a/lib/widgets/dialogs/entry_editors/tag_editor_page.dart +++ b/lib/widgets/dialogs/entry_editors/tag_editor_page.dart @@ -6,6 +6,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/expandable_filter_row.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; @@ -41,6 +42,8 @@ class _TagEditorPageState extends State { @override void initState() { super.initState(); + _expandedSectionNotifier.value = settings.tagEditorExpandedSection; + _expandedSectionNotifier.addListener(() => settings.tagEditorExpandedSection = _expandedSectionNotifier.value); _initTopTags(); } @@ -61,7 +64,7 @@ class _TagEditorPageState extends State { }); List> sortedTags = _sortCurrentTags(entryCountByTag); - return Scaffold( + return AvesScaffold( appBar: AppBar( title: Text(l10n.tagEditorPageTitle), actions: [ diff --git a/lib/widgets/dialogs/export_entry_dialog.dart b/lib/widgets/dialogs/export_entry_dialog.dart index e0e51f3c5..6cce70cde 100644 --- a/lib/widgets/dialogs/export_entry_dialog.dart +++ b/lib/widgets/dialogs/export_entry_dialog.dart @@ -9,6 +9,8 @@ import 'package:flutter/material.dart'; import 'aves_dialog.dart'; class ExportEntryDialog extends StatefulWidget { + static const routeName = '/dialog/export_entry'; + final AvesEntry entry; const ExportEntryDialog({ @@ -130,7 +132,7 @@ class _ExportEntryDialogState extends State { height: height, ) : null; - Navigator.pop(context, options); + Navigator.maybeOf(context)?.pop(options); } : null, child: Text(l10n.applyButtonLabel), diff --git a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart index 3bcf76bc7..3dde93755 100644 --- a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart @@ -23,6 +23,8 @@ import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class CoverSelectionDialog extends StatefulWidget { + static const routeName = '/dialog/select_cover'; + final CollectionFilter filter; final AvesEntry? customEntry; final String? customPackage; @@ -161,7 +163,7 @@ class _CoverSelectionDialogState extends State { final entry = _isCustomEntry ? _customEntry : null; final package = _isCustomPackage ? _customPackage : null; final color = _isCustomColor ? _customColor : null; - return Navigator.pop(context, Tuple3(entry, package, color)); + return Navigator.maybeOf(context)?.pop(Tuple3(entry, package, color)); }, child: Text(l10n.applyButtonLabel), ) @@ -340,8 +342,7 @@ class _CoverSelectionDialogState extends State { } Future _pickEntry() async { - final entry = await Navigator.push( - context, + final entry = await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: ItemPickPage.routeName), builder: (context) => ItemPickPage( @@ -361,8 +362,7 @@ class _CoverSelectionDialogState extends State { } Future _pickPackage() async { - final package = await Navigator.push( - context, + final package = await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: AppPickPage.routeName), builder: (context) => AppPickPage( @@ -386,6 +386,7 @@ class _CoverSelectionDialogState extends State { // picker controls are not on edge and palette panel is more stable initialValue: _customColor ?? const Color(0xff3f51b5), ), + routeSettings: const RouteSettings(name: ColorPickerDialog.routeName), ); if (color != null) { _customColor = color; diff --git a/lib/widgets/dialogs/filter_editors/create_album_dialog.dart b/lib/widgets/dialogs/filter_editors/create_album_dialog.dart index f0839e8fe..def2ecef2 100644 --- a/lib/widgets/dialogs/filter_editors/create_album_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/create_album_dialog.dart @@ -4,12 +4,13 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import '../aves_dialog.dart'; - class CreateAlbumDialog extends StatefulWidget { + static const routeName = '/dialog/create_album'; + const CreateAlbumDialog({super.key}); @override @@ -159,7 +160,7 @@ class _CreateAlbumDialogState extends State { void _submit(BuildContext context) { if (_isValidNotifier.value) { - Navigator.pop(context, _buildAlbumPath(_nameController.text)); + Navigator.maybeOf(context)?.pop(_buildAlbumPath(_nameController.text)); } } } diff --git a/lib/widgets/dialogs/filter_editors/edit_vault_dialog.dart b/lib/widgets/dialogs/filter_editors/edit_vault_dialog.dart new file mode 100644 index 000000000..52c81df08 --- /dev/null +++ b/lib/widgets/dialogs/filter_editors/edit_vault_dialog.dart @@ -0,0 +1,184 @@ +import 'package:aves/model/device.dart'; +import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/vaults/details.dart'; +import 'package:aves/model/vaults/enums.dart'; +import 'package:aves/model/vaults/vaults.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_caption.dart'; +import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class EditVaultDialog extends StatefulWidget { + static const routeName = '/dialog/edit_vault'; + + final VaultDetails? initialDetails; + + const EditVaultDialog({ + super.key, + this.initialDetails, + }); + + @override + State createState() => _EditVaultDialogState(); +} + +class _EditVaultDialogState extends State { + final TextEditingController _nameController = TextEditingController(); + late bool _useBin; + late bool _autoLockScreenOff; + late VaultLockType _lockType; + + final ValueNotifier _existsNotifier = ValueNotifier(false); + final ValueNotifier _isValidNotifier = ValueNotifier(false); + + final List _lockTypeOptions = [ + if (device.canAuthenticateUser) VaultLockType.system, + if (device.canUseCrypto) ...[ + VaultLockType.pin, + VaultLockType.password, + ], + ]; + + VaultDetails? get initialDetails => widget.initialDetails; + + String get newName => _nameController.text; + + @override + void initState() { + super.initState(); + final details = initialDetails ?? + VaultDetails( + name: '', + autoLockScreenOff: true, + useBin: settings.enableBin, + lockType: _lockTypeOptions.first, + ); + _nameController.text = details.name; + _useBin = details.useBin; + _autoLockScreenOff = details.autoLockScreenOff; + _lockType = details.lockType; + _validate(); + } + + @override + void dispose() { + _nameController.dispose(); + _existsNotifier.dispose(); + _isValidNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final isNew = initialDetails == null; + return AvesDialog( + title: isNew ? l10n.newVaultDialogTitle : l10n.configureVaultDialogTitle, + scrollableContent: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ValueListenableBuilder( + valueListenable: _existsNotifier, + builder: (context, exists, child) { + return TextField( + controller: _nameController, + decoration: InputDecoration( + labelText: l10n.newAlbumDialogNameLabel, + helperText: exists ? l10n.newAlbumDialogNameLabelAlreadyExistsHelper : '', + ), + onChanged: (_) => _validate(), + onSubmitted: (_) => _submit(context), + ); + }), + ), + if (_lockTypeOptions.length > 1) + ListTile( + title: Text(l10n.vaultDialogLockTypeLabel), + subtitle: AvesCaption(_lockType.getText(context)), + onTap: () { + _unfocus(); + showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: _lockType, + options: Map.fromEntries(_lockTypeOptions.map((v) => MapEntry(v, v.getText(context)))), + ), + onSelection: (v) => setState(() => _lockType = v), + ); + }, + ), + SwitchListTile( + value: _autoLockScreenOff, + onChanged: (v) => setState(() => _autoLockScreenOff = v), + title: Text(l10n.vaultDialogLockModeWhenScreenOff), + ), + if (settings.enableBin) + SwitchListTile( + value: _useBin, + onChanged: (v) async { + if (!v) { + final album = initialDetails?.path; + if (album != null) { + final filter = AlbumFilter(album, null); + final source = context.read(); + if (source.trashedEntries.any(filter.test)) { + if (!await showConfirmationDialog( + context: context, + message: l10n.settingsDisablingBinWarningDialogMessage, + confirmationButtonLabel: l10n.applyButtonLabel, + )) return; + } + } + } + setState(() => _useBin = v); + }, + title: Text(l10n.settingsEnableBin), + ), + ], + actions: [ + const CancelButton(), + ValueListenableBuilder( + valueListenable: _isValidNotifier, + builder: (context, isValid, child) { + return TextButton( + onPressed: isValid ? () => _submit(context) : null, + child: Text(isNew ? l10n.createAlbumButtonLabel : l10n.applyButtonLabel), + ); + }, + ), + ], + ); + } + + // remove focus, if any, to prevent the keyboard from showing up + // after the user is done with the dialog + void _unfocus() => FocusManager.instance.primaryFocus?.unfocus(); + + Future _validate() async { + final notEmpty = newName.isNotEmpty; + final exists = notEmpty && vaults.all.map((v) => v.name).contains(newName) && newName != initialDetails?.name; + _existsNotifier.value = exists; + _isValidNotifier.value = notEmpty && !exists; + } + + Future _submit(BuildContext context) async { + if (!_isValidNotifier.value) return; + + _unfocus(); + + final details = VaultDetails( + name: newName, + autoLockScreenOff: _autoLockScreenOff, + useBin: _useBin, + lockType: _lockType, + ); + if (!await vaults.setPass(context, details)) return; + + Navigator.maybeOf(context)?.pop(details); + } +} diff --git a/lib/widgets/dialogs/filter_editors/password_dialog.dart b/lib/widgets/dialogs/filter_editors/password_dialog.dart new file mode 100644 index 000000000..1bce02e90 --- /dev/null +++ b/lib/widgets/dialogs/filter_editors/password_dialog.dart @@ -0,0 +1,65 @@ +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:flutter/material.dart'; + +class PasswordDialog extends StatefulWidget { + static const routeName = '/dialog/password'; + + final bool needConfirmation; + + const PasswordDialog({ + super.key, + required this.needConfirmation, + }); + + @override + State createState() => _PasswordDialogState(); +} + +class _PasswordDialogState extends State { + final _controller = TextEditingController(); + final _focusNode = FocusNode(); + bool _confirming = false; + String? _firstPassword; + + @override + void initState() { + super.initState(); + _focusNode.requestFocus(); + } + + @override + Widget build(BuildContext context) { + return AvesDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_confirming ? context.l10n.passwordDialogConfirm : context.l10n.passwordDialogEnter), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: TextField( + controller: _controller, + focusNode: _focusNode, + obscureText: true, + onSubmitted: (password) { + if (widget.needConfirmation) { + if (_confirming) { + Navigator.maybeOf(context)?.pop(_firstPassword == password ? password : null); + } else { + _firstPassword = password; + _controller.clear(); + setState(() => _confirming = true); + WidgetsBinding.instance.addPostFrameCallback((_) => _focusNode.requestFocus()); + } + } else { + Navigator.maybeOf(context)?.pop(password); + } + }, + autofillHints: const [AutofillHints.password], + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/dialogs/filter_editors/pin_dialog.dart b/lib/widgets/dialogs/filter_editors/pin_dialog.dart new file mode 100644 index 000000000..6afd18420 --- /dev/null +++ b/lib/widgets/dialogs/filter_editors/pin_dialog.dart @@ -0,0 +1,65 @@ +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:pinput/pinput.dart'; + +class PinDialog extends StatefulWidget { + static const routeName = '/dialog/pin'; + + final bool needConfirmation; + + const PinDialog({ + super.key, + required this.needConfirmation, + }); + + @override + State createState() => _PinDialogState(); +} + +class _PinDialogState extends State { + final _controller = TextEditingController(); + final _focusNode = FocusNode(); + bool _confirming = false; + String? _firstPin; + + @override + void initState() { + super.initState(); + _focusNode.requestFocus(); + } + + @override + Widget build(BuildContext context) { + return AvesDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_confirming ? context.l10n.pinDialogConfirm : context.l10n.pinDialogEnter), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Pinput( + onCompleted: (pin) { + if (widget.needConfirmation) { + if (_confirming) { + Navigator.maybeOf(context)?.pop(_firstPin == pin ? pin : null); + } else { + _firstPin = pin; + _controller.clear(); + setState(() => _confirming = true); + WidgetsBinding.instance.addPostFrameCallback((_) => _focusNode.requestFocus()); + } + } else { + Navigator.maybeOf(context)?.pop(pin); + } + }, + controller: _controller, + focusNode: _focusNode, + obscureText: true, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/dialogs/filter_editors/rename_album_dialog.dart b/lib/widgets/dialogs/filter_editors/rename_album_dialog.dart index e18abe61e..0d254a30b 100644 --- a/lib/widgets/dialogs/filter_editors/rename_album_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/rename_album_dialog.dart @@ -6,6 +6,8 @@ import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:flutter/material.dart'; class RenameAlbumDialog extends StatefulWidget { + static const routeName = '/dialog/rename_album'; + final String album; const RenameAlbumDialog({ @@ -88,7 +90,7 @@ class _RenameAlbumDialogState extends State { void _submit(BuildContext context) { if (_isValidNotifier.value) { - Navigator.pop(context, _nameController.text); + Navigator.maybeOf(context)?.pop(_nameController.text); } } } diff --git a/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart index 7039ccfe4..a391265e5 100644 --- a/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart +++ b/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart @@ -4,10 +4,13 @@ import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/selection.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves/model/vaults/details.dart'; +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/basic/menu.dart'; @@ -16,7 +19,9 @@ 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/selection_provider.dart'; +import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart'; +import 'package:aves/widgets/dialogs/filter_editors/edit_vault_dialog.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/album_set.dart'; import 'package:aves/widgets/filter_grids/common/app_bar.dart'; @@ -35,8 +40,7 @@ Future pickAlbum({ // source may not be fully initialized in view mode await source.init(); } - final filter = await Navigator.push( - context, + final filter = await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: _AlbumPickPage.routeName), builder: (context) => _AlbumPickPage(source: source, moveType: moveType), @@ -80,6 +84,19 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { } } + static const _quickActions = [ + ChipSetAction.createAlbum, + ]; + + // `null` items are converted to dividers + static const _menuActions = [ + ...ChipSetActions.general, + null, + ChipSetAction.toggleTitleSearch, + null, + ChipSetAction.createVault, + ]; + @override Widget build(BuildContext context) { return ListenableProvider>.value( @@ -142,23 +159,37 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { selectedFilters: selectedFilters, ); + void onActionSelected(ChipSetAction action) { + switch (action) { + case ChipSetAction.createAlbum: + _createAlbum(); + break; + case ChipSetAction.createVault: + _createVault(); + break; + default: + actionDelegate.onActionSelected(context, {}, action); + break; + } + } + return settings.useTvLayout ? _buildTelevisionActions( context: context, isVisible: isVisible, - actionDelegate: actionDelegate, + onActionSelected: onActionSelected, ) : _buildMobileActions( context: context, isVisible: isVisible, - actionDelegate: actionDelegate, + onActionSelected: onActionSelected, ); } List _buildTelevisionActions({ required BuildContext context, required bool Function(ChipSetAction action) isVisible, - required AlbumChipSetActionDelegate actionDelegate, + required void Function(ChipSetAction action) onActionSelected, }) { return [ ...ChipSetActions.general, @@ -166,7 +197,7 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { return CaptionedButton( icon: action.getIcon(), caption: action.getText(context), - onPressed: () => actionDelegate.onActionSelected(context, {}, action), + onPressed: () => onActionSelected(action), ); }).toList(); } @@ -174,33 +205,22 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { List _buildMobileActions({ required BuildContext context, required bool Function(ChipSetAction action) isVisible, - required AlbumChipSetActionDelegate actionDelegate, + required void Function(ChipSetAction action) onActionSelected, }) { return [ if (widget.moveType != null) - IconButton( - icon: const Icon(AIcons.add), - onPressed: () async { - final newAlbum = await showDialog( - context: context, - builder: (context) => const CreateAlbumDialog(), - ); - // wait for the dialog to hide as applying the change may block the UI - await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); - if (newAlbum != null && newAlbum.isNotEmpty) { - Navigator.pop(context, AlbumFilter(newAlbum, source.getAlbumDisplayName(context, newAlbum))); - } - }, - tooltip: context.l10n.createAlbumTooltip, - ), + ..._quickActions.where(isVisible).map((action) => IconButton( + icon: action.getIcon(), + onPressed: () => onActionSelected(action), + tooltip: action.getText(context), + )), MenuIconTheme( child: PopupMenuButton( itemBuilder: (context) { - return [ - ...ChipSetActions.general.where(isVisible).map((action) => FilterGridAppBar.toMenuItem(context, action, enabled: true)), - const PopupMenuDivider(), - FilterGridAppBar.toMenuItem(context, ChipSetAction.toggleTitleSearch, enabled: true), - ]; + return _menuActions.where((v) => v == null || isVisible(v)).map((action) { + if (action == null) return const PopupMenuDivider(); + return FilterGridAppBar.toMenuItem(context, action, enabled: true); + }).toList(); }, onSelected: (action) async { // remove focus, if any, to prevent the keyboard from showing up @@ -209,10 +229,53 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { // wait for the popup menu to hide before proceeding with the action await Future.delayed(Durations.popupMenuAnimation * timeDilation); - actionDelegate.onActionSelected(context, {}, action); + onActionSelected(action); }, ), ), ]; } + + Future _createAlbum() async { + final directory = await showDialog( + context: context, + builder: (context) => const CreateAlbumDialog(), + routeSettings: const RouteSettings(name: CreateAlbumDialog.routeName), + ); + if (directory == null) return; + + // wait for the dialog to hide as applying the change may block the UI + await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); + + _pickAlbum(directory); + } + + Future _createVault() async { + final l10n = context.l10n; + if (!await showSkippableConfirmationDialog( + context: context, + type: ConfirmationDialog.createVault, + message: l10n.newVaultWarningDialogMessage, + confirmationButtonLabel: l10n.continueButtonLabel, + )) return; + + final details = await showDialog( + context: context, + builder: (context) => const EditVaultDialog(), + routeSettings: const RouteSettings(name: EditVaultDialog.routeName), + ); + if (details == null) return; + + // wait for the dialog to hide as applying the change may block the UI + await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); + + await vaults.create(details); + _pickAlbum(details.path); + } + + void _pickAlbum(String directory) { + source.createAlbum(directory); + final filter = AlbumFilter(directory, source.getAlbumDisplayName(context, directory)); + Navigator.maybeOf(context)?.pop(filter); + } } diff --git a/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart index 418edc63f..e49e7334b 100644 --- a/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart +++ b/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart @@ -4,6 +4,7 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/basic/query_bar.dart'; import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -39,7 +40,7 @@ class _AppPickPageState extends State { @override Widget build(BuildContext context) { final useTvLayout = settings.useTvLayout; - return Scaffold( + return AvesScaffold( appBar: AppBar( automaticallyImplyLeading: !useTvLayout, title: Text(context.l10n.appPickDialogTitle), @@ -83,7 +84,7 @@ class _AppPickPageState extends State { return ReselectableRadioListTile( value: '', groupValue: _selectedValue, - onChanged: (v) => Navigator.pop(context, v), + onChanged: (v) => Navigator.maybeOf(context)?.pop(v), reselectable: true, title: Text( context.l10n.appPickDialogNone, @@ -100,7 +101,7 @@ class _AppPickPageState extends State { return ReselectableRadioListTile( value: package.packageName, groupValue: _selectedValue, - onChanged: (v) => Navigator.pop(context, v), + onChanged: (v) => Navigator.maybeOf(context)?.pop(v), reselectable: true, title: Text.rich( TextSpan( diff --git a/lib/widgets/dialogs/pick_dialogs/item_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/item_pick_page.dart index 7f84a01a8..72ad90cc0 100644 --- a/lib/widgets/dialogs/pick_dialogs/item_pick_page.dart +++ b/lib/widgets/dialogs/pick_dialogs/item_pick_page.dart @@ -5,6 +5,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/providers/query_provider.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart'; import 'package:collection/collection.dart'; @@ -39,7 +40,7 @@ class _ItemPickPageState extends State { final liveFilter = collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?; return ListenableProvider>.value( value: ValueNotifier(AppMode.pickMediaInternal), - child: Scaffold( + child: AvesScaffold( body: SelectionProvider( child: QueryProvider( initialQuery: liveFilter?.query, diff --git a/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart index 930c55bd3..a14a15fad 100644 --- a/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart +++ b/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart @@ -10,6 +10,7 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/debouncer.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons/outlined_button.dart'; import 'package:aves/widgets/common/map/geo_map.dart'; @@ -34,7 +35,7 @@ class LocationPickPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( + return AvesScaffold( body: SafeArea( left: false, top: false, @@ -118,7 +119,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin const SizedBox(height: 8), AvesOutlinedButton( label: context.l10n.locationPickerUseThisLocationButton, - onPressed: () => Navigator.pop(context, _dotLocationNotifier.value), + onPressed: () => Navigator.maybeOf(context)?.pop(_dotLocationNotifier.value), ), ], ), diff --git a/lib/widgets/dialogs/tile_view_dialog.dart b/lib/widgets/dialogs/tile_view_dialog.dart index 7f5272c0c..077cedee1 100644 --- a/lib/widgets/dialogs/tile_view_dialog.dart +++ b/lib/widgets/dialogs/tile_view_dialog.dart @@ -14,6 +14,8 @@ import 'package:tuple/tuple.dart'; import 'aves_dialog.dart'; class TileViewDialog extends StatefulWidget { + static const routeName = '/dialog/tile_view'; + final Tuple4 initialValue; final List> sortOptions; final List> groupOptions; @@ -150,7 +152,7 @@ class _TileViewDialogState extends State> with key: const Key('button-apply'), onPressed: () { tileExtentController.setUserPreferredColumnCount(_columnCountNotifier.value); - Navigator.pop(context, Tuple4(_selectedSort, _selectedGroup, _selectedLayout, _reverseSort)); + Navigator.maybeOf(context)?.pop(Tuple4(_selectedSort, _selectedGroup, _selectedLayout, _reverseSort)); }, child: Text(l10n.applyButtonLabel), ) @@ -170,53 +172,76 @@ class _TileViewDialogState extends State> with }) { if (options.isEmpty || !show) return const SizedBox(); + final label = ConstrainedBox( + constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), + child: Row( + children: [ + Icon(icon), + const SizedBox(width: 16), + Expanded( + child: HighlightTitle( + title: title, + showHighlight: false, + ), + ), + if (trailing != null) trailing, + ], + ), + ); + final selector = TextDropdownButton( + values: options.map((v) => v.value).toList(), + valueText: (v) => options.firstWhere((option) => option.value == v).title, + valueIcon: (v) => options.firstWhere((option) => option.value == v).icon, + value: value, + onChanged: (v) => setState(() => onChanged(v)), + isExpanded: true, + dropdownColor: Themes.thirdLayerColor(context), + ); + final iconSize = IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context); + final isPortrait = context.select((mq) => mq.orientation) == Orientation.portrait; + final child = isPortrait + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + label, + Padding( + padding: EdgeInsetsDirectional.only(start: iconSize + 16, end: 12), + child: selector, + ), + if (bottom != null) + Padding( + padding: EdgeInsetsDirectional.only(start: iconSize + 16), + child: bottom, + ), + ], + ) + : Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: label), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + selector, + if (bottom != null) bottom, + ], + ), + ), + ], + ); + return TooltipTheme( data: TooltipTheme.of(context).copyWith( preferBelow: false, ), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - ConstrainedBox( - constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), - child: Row( - children: [ - Icon(icon), - const SizedBox(width: 16), - Expanded( - child: HighlightTitle( - title: title, - showHighlight: false, - ), - ), - if (trailing != null) trailing, - ], - ), - ), - Padding( - padding: EdgeInsetsDirectional.only(start: iconSize + 16, end: 12), - child: TextDropdownButton( - values: options.map((v) => v.value).toList(), - valueText: (v) => options.firstWhere((option) => option.value == v).title, - valueIcon: (v) => options.firstWhere((option) => option.value == v).icon, - value: value, - onChanged: (v) => setState(() => onChanged(v)), - isExpanded: true, - dropdownColor: Themes.thirdLayerColor(context), - ), - ), - if (bottom != null) - Padding( - padding: EdgeInsetsDirectional.only(start: iconSize + 16), - child: bottom, - ), - ], - ), + padding: const EdgeInsets.only(left: 16, top: 8, right: 16), + child: child, ), ); } diff --git a/lib/widgets/dialogs/video_speed_dialog.dart b/lib/widgets/dialogs/video_speed_dialog.dart index f835e1026..b38073f9d 100644 --- a/lib/widgets/dialogs/video_speed_dialog.dart +++ b/lib/widgets/dialogs/video_speed_dialog.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'aves_dialog.dart'; class VideoSpeedDialog extends StatefulWidget { + static const routeName = '/dialog/select_video_speed'; + final double current, min, max; const VideoSpeedDialog({ @@ -66,5 +68,5 @@ class _VideoSpeedDialogState extends State { ); } - void _submit(BuildContext context) => Navigator.pop(context, _speed); + void _submit(BuildContext context) => Navigator.maybeOf(context)?.pop(_speed); } diff --git a/lib/widgets/dialogs/video_stream_selection_dialog.dart b/lib/widgets/dialogs/video_stream_selection_dialog.dart index eef7d88d9..eb0e65350 100644 --- a/lib/widgets/dialogs/video_stream_selection_dialog.dart +++ b/lib/widgets/dialogs/video_stream_selection_dialog.dart @@ -11,6 +11,8 @@ import 'package:flutter/material.dart'; import 'aves_dialog.dart'; class VideoStreamSelectionDialog extends StatefulWidget { + static const routeName = '/dialog/select_video_stream'; + final Map streams; const VideoStreamSelectionDialog({ @@ -153,7 +155,7 @@ class _VideoStreamSelectionDialogState extends State ]; } - void _submit(BuildContext context) => Navigator.pop(context, { + void _submit(BuildContext context) => Navigator.maybeOf(context)?.pop({ StreamType.video: _currentVideo, StreamType.audio: _currentAudio, StreamType.text: _currentText, diff --git a/lib/widgets/dialogs/wallpaper_settings_dialog.dart b/lib/widgets/dialogs/wallpaper_settings_dialog.dart index 5dc85114d..0e6d46fad 100644 --- a/lib/widgets/dialogs/wallpaper_settings_dialog.dart +++ b/lib/widgets/dialogs/wallpaper_settings_dialog.dart @@ -8,6 +8,8 @@ import 'package:tuple/tuple.dart'; import 'aves_dialog.dart'; class WallpaperSettingsDialog extends StatefulWidget { + static const routeName = '/dialog/wallpaper_settings'; + const WallpaperSettingsDialog({super.key}); @override @@ -41,7 +43,7 @@ class _WallpaperSettingsDialogState extends State { actions: [ const CancelButton(), TextButton( - onPressed: () => Navigator.pop(context, Tuple2(_selectedTarget, _useScrollEffect)), + onPressed: () => Navigator.maybeOf(context)?.pop(Tuple2(_selectedTarget, _useScrollEffect)), child: Text(context.l10n.applyButtonLabel), ), ], diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index eade6bd8f..b39500edd 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -99,6 +99,7 @@ class AlbumListPage extends StatelessWidget { case AlbumChipGroupFactor.importance: final specialKey = AlbumImportanceSectionKey.special(context); final appsKey = AlbumImportanceSectionKey.apps(context); + final vaultKey = AlbumImportanceSectionKey.vault(context); final regularKey = AlbumImportanceSectionKey.regular(context); sections = groupBy, ChipSectionKey>(unpinnedMapEntries, (kv) { switch (covers.effectiveAlbumType(kv.filter.album)) { @@ -106,6 +107,8 @@ class AlbumListPage extends StatelessWidget { return regularKey; case AlbumType.app: return appsKey; + case AlbumType.vault: + return vaultKey; default: return specialKey; } @@ -115,6 +118,7 @@ class AlbumListPage extends StatelessWidget { // group ordering if (sections.containsKey(specialKey)) specialKey: sections[specialKey]!, if (sections.containsKey(appsKey)) appsKey: sections[appsKey]!, + if (sections.containsKey(vaultKey)) vaultKey: sections[vaultKey]!, if (sections.containsKey(regularKey)) regularKey: sections[regularKey]!, }; break; 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 843a5eda3..1565080f7 100644 --- a/lib/widgets/filter_grids/common/action_delegates/album_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/album_set.dart @@ -3,14 +3,19 @@ import 'dart:io'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/chip_set_actions.dart'; import 'package:aves/model/actions/move_type.dart'; +import 'package:aves/model/device.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/highlight.dart'; import 'package:aves/model/selection.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/enums/view.dart'; +import 'package:aves/model/vaults/details.dart'; +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/enums.dart'; @@ -18,10 +23,13 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/action_mixins/entry_storage.dart'; +import 'package:aves/widgets/common/action_mixins/vault_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; +import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart'; +import 'package:aves/widgets/dialogs/filter_editors/edit_vault_dialog.dart'; import 'package:aves/widgets/dialogs/filter_editors/rename_album_dialog.dart'; import 'package:aves/widgets/dialogs/tile_view_dialog.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; @@ -32,7 +40,7 @@ import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; -class AlbumChipSetActionDelegate extends ChipSetActionDelegate with EntryStorageMixin { +class AlbumChipSetActionDelegate extends ChipSetActionDelegate with EntryStorageMixin, VaultAwareMixin { final Iterable> _items; AlbumChipSetActionDelegate(Iterable> items) : _items = items; @@ -73,12 +81,24 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with required int itemCount, required Set selectedFilters, }) { + final selectedSingleItem = selectedFilters.length == 1; + final isMain = appMode == AppMode.main; + + final canCreate = !settings.isReadOnly && appMode.canCreateFilter && !isSelecting; switch (action) { case ChipSetAction.createAlbum: - return !settings.isReadOnly && appMode == AppMode.main && !isSelecting; + return canCreate; + case ChipSetAction.createVault: + return canCreate && device.canUseVaults; case ChipSetAction.delete: case ChipSetAction.rename: - return !settings.isReadOnly && appMode == AppMode.main && isSelecting; + return isMain && isSelecting && !settings.isReadOnly; + case ChipSetAction.hide: + return isMain && selectedFilters.none((v) => vaults.isVault(v.album)); + case ChipSetAction.configureVault: + return isMain && selectedSingleItem && vaults.isVault(selectedFilters.first.album); + case ChipSetAction.lockVault: + return isMain && selectedFilters.any((v) => vaults.isVault(v.album)); default: return super.isVisible( action, @@ -97,14 +117,25 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with required int itemCount, required Set selectedFilters, }) { + final selectedItemCount = selectedFilters.length; + final hasSelection = selectedItemCount > 0; + switch (action) { case ChipSetAction.rename: - { - if (selectedFilters.length != 1) return false; - // do not allow renaming volume root - final dir = VolumeRelativeDirectory.fromPath(selectedFilters.first.album); - return dir != null && dir.relativeDir.isNotEmpty; - } + if (selectedFilters.length != 1) return false; + + final dirPath = selectedFilters.first.album; + if (vaults.isVault(dirPath)) return true; + + // do not allow renaming volume root + final dir = VolumeRelativeDirectory.fromPath(dirPath); + return dir != null && dir.relativeDir.isNotEmpty; + case ChipSetAction.hide: + return hasSelection; + case ChipSetAction.lockVault: + return selectedFilters.map((v) => v.album).any((v) => vaults.isVault(v) && !vaults.isLocked(v)); + case ChipSetAction.configureVault: + return true; default: return super.canApply( action, @@ -117,19 +148,30 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with @override void onActionSelected(BuildContext context, Set filters, ChipSetAction action) { + reportService.log('$action'); switch (action) { // general case ChipSetAction.createAlbum: - _createAlbum(context); + _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; } @@ -159,6 +201,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with tileExtentController: extentController, ); }, + routeSettings: const RouteSettings(name: TileViewDialog.routeName), ); // wait for the dialog to hide as applying the change may block the UI await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); @@ -170,51 +213,92 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with } } - void _createAlbum(BuildContext context) async { - final newAlbum = await showDialog( - context: context, - builder: (context) => const CreateAlbumDialog(), - ); - if (newAlbum != null && newAlbum.isNotEmpty) { - final source = context.read(); - source.createAlbum(newAlbum); + void _createAlbum(BuildContext context, {required bool locked}) async { + final l10n = context.l10n; + final source = context.read(); + late final String? directory; + if (locked) { + if (!await showSkippableConfirmationDialog( + context: context, + type: ConfirmationDialog.createVault, + message: l10n.newVaultWarningDialogMessage, + confirmationButtonLabel: l10n.continueButtonLabel, + )) return; - final showAction = SnackBarAction( - label: context.l10n.showButtonLabel, - onPressed: () async { - // local context may be deactivated when action is triggered after navigation - final context = AvesApp.navigatorKey.currentContext; - if (context != null) { - final highlightInfo = context.read(); - final filter = AlbumFilter(newAlbum, source.getAlbumDisplayName(context, newAlbum)); - if (context.currentRouteName == AlbumListPage.routeName) { - highlightInfo.trackItem(FilterGridItem(filter, null), highlightItem: filter); - } else { - highlightInfo.set(filter); - await Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - settings: const RouteSettings(name: AlbumListPage.routeName), - builder: (_) => const AlbumListPage(), - ), - (route) => false, - ); - } - } - }, + final details = await showDialog( + context: context, + builder: (context) => const EditVaultDialog(), + routeSettings: const RouteSettings(name: CreateAlbumDialog.routeName), ); - showFeedback(context, context.l10n.genericSuccessFeedback, showAction); + if (details == null) return; + + await vaults.create(details); + directory = details.path; + } else { + directory = await showDialog( + context: context, + builder: (context) => const CreateAlbumDialog(), + routeSettings: const RouteSettings(name: CreateAlbumDialog.routeName), + ); + if (directory == null) return; } + source.createAlbum(directory); + + final filter = AlbumFilter(directory, source.getAlbumDisplayName(context, directory)); + final showAction = SnackBarAction( + label: l10n.showButtonLabel, + onPressed: () async { + // local context may be deactivated when action is triggered after navigation + final context = AvesApp.navigatorKey.currentContext; + if (context != null) { + final highlightInfo = context.read(); + if (context.currentRouteName == AlbumListPage.routeName) { + highlightInfo.trackItem(FilterGridItem(filter, null), highlightItem: filter); + } else { + highlightInfo.set(filter); + await Navigator.maybeOf(context)?.pushAndRemoveUntil( + MaterialPageRoute( + settings: const RouteSettings(name: AlbumListPage.routeName), + builder: (_) => const AlbumListPage(), + ), + (route) => false, + ); + } + } + }, + ); + showFeedback(context, l10n.genericSuccessFeedback, showAction); } Future _delete(BuildContext context, Set filters) async { + final byBinUsage = groupBy(filters, (filter) { + final details = vaults.getVault(filter.album); + return details?.useBin ?? settings.enableBin; + }); + await Future.forEach( + byBinUsage.entries, + (kv) => _doDelete( + context: context, + filters: kv.value.toSet(), + enableBin: kv.key, + )); + _browse(context); + } + + Future _doDelete({ + required BuildContext context, + required Set filters, + required bool enableBin, + }) async { + if (!await unlockFilters(context, filters)) return; + final source = context.read(); final todoEntries = source.visibleEntries.where((entry) => filters.any((f) => f.test(entry))).toSet(); final todoAlbums = filters.map((v) => v.album).toSet(); final filledAlbums = todoEntries.map((e) => e.directory).whereNotNull().toSet(); final emptyAlbums = todoAlbums.whereNot(filledAlbums.contains).toSet(); - if (settings.enableBin && filledAlbums.isNotEmpty) { + if (enableBin && filledAlbums.isNotEmpty) { await doMove( context, moveType: MoveType.toBin, @@ -229,7 +313,6 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with } final l10n = context.l10n; - final messenger = ScaffoldMessenger.of(context); final todoCount = todoEntries.length; final confirmed = await showDialog( @@ -239,11 +322,12 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with actions: [ const CancelButton(), TextButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () => Navigator.maybeOf(context)?.pop(true), child: Text(l10n.deleteButtonLabel), ), ], ), + routeSettings: const RouteSettings(name: AvesDialog.confirmationRouteName), ); if (confirmed == null || !confirmed) return; @@ -252,6 +336,26 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with if (!await checkStoragePermissionForAlbums(context, filledAlbums)) return; + await _deleteEntriesForever(context, todoEntries); + + final vaultAlbumFilters = filters.where((v) => vaults.isVault(v.album)).toSet(); + if (vaultAlbumFilters.isNotEmpty) { + final allEntries = source.allEntries; + final emptyVaultAlbums = vaultAlbumFilters.whereNot((v) => allEntries.any(v.test)).map((v) => v.album).toSet(); + await vaults.remove(emptyVaultAlbums); + } + } + + Future _deleteEntriesForever(BuildContext context, Set todoEntries) async { + if (todoEntries.isEmpty) return; + + final source = context.read(); + final filledAlbums = todoEntries.map((e) => e.directory).whereNotNull().toSet(); + + final l10n = context.l10n; + final messenger = ScaffoldMessenger.of(context); + final todoCount = todoEntries.length; + source.pauseMonitoring(); final opId = mediaEditService.newOpId; await showOpReport( @@ -280,6 +384,34 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with } Future _rename(BuildContext context, AlbumFilter filter) async { + if (!await unlockFilter(context, filter)) return; + + final album = filter.album; + if (!vaults.isVault(album)) { + final dir = VolumeRelativeDirectory.fromPath(album); + // do not allow renaming volume root + if (dir == null || dir.relativeDir.isEmpty) return; + + // check whether renaming is possible given OS restrictions, + // before asking to input a new name + final restrictedDirs = await storageService.getRestrictedDirectories(); + if (restrictedDirs.contains(dir)) { + await showRestrictedDirectoryDialog(context, dir); + return; + } + } + + final newName = await showDialog( + context: context, + builder: (context) => RenameAlbumDialog(album: album), + routeSettings: const RouteSettings(name: RenameAlbumDialog.routeName), + ); + if (newName == null || newName.isEmpty) return; + + await _doRename(context, filter, newName); + } + + Future _doRename(BuildContext context, AlbumFilter filter, String newName) async { final l10n = context.l10n; final messenger = ScaffoldMessenger.of(context); final source = context.read(); @@ -287,24 +419,6 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with final todoEntries = source.visibleEntries.where(filter.test).toSet(); final todoCount = todoEntries.length; - final dir = VolumeRelativeDirectory.fromPath(album); - // do not allow renaming volume root - if (dir == null || dir.relativeDir.isEmpty) return; - - // check whether renaming is possible given OS restrictions, - // before asking to input a new name - final restrictedDirs = await storageService.getRestrictedDirectories(); - if (restrictedDirs.contains(dir)) { - await showRestrictedDirectoryDialog(context, dir); - return; - } - - final newName = await showDialog( - context: context, - builder: (context) => RenameAlbumDialog(album: album), - ); - if (newName == null || newName.isEmpty) return; - final destinationAlbumParent = pContext.dirname(album); final destinationAlbum = pContext.join(destinationAlbumParent, newName); if (!await checkFreeSpaceForMove(context, todoEntries, destinationAlbum, MoveType.move)) return; @@ -349,4 +463,37 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with }, ); } + + Future _configureVault(BuildContext context, AlbumFilter filter) async { + if (!await unlockFilter(context, filter)) return; + + final oldDetails = vaults.getVault(filter.album); + if (oldDetails == null) return; + + final newDetails = await showDialog( + context: context, + builder: (context) => EditVaultDialog(initialDetails: oldDetails), + routeSettings: const RouteSettings(name: EditVaultDialog.routeName), + ); + if (newDetails == null || oldDetails == newDetails) return; + + if (oldDetails.useBin && !newDetails.useBin) { + final filter = AlbumFilter(oldDetails.path, null); + final source = context.read(); + await _deleteEntriesForever(context, source.trashedEntries.where(filter.test).toSet()); + } + + final oldName = oldDetails.name; + final newName = newDetails.name; + if (oldName != newName) { + await vaults.update(newDetails.copyWith(name: oldName)); + // wipe the old pass, if any, so that it does not overwrite the new pass + // when renaming the vault afterwards + await securityService.writeValue(oldDetails.passKey, null); + await _doRename(context, filter, newName); + } else { + await vaults.update(newDetails); + _browse(context); + } + } } diff --git a/lib/widgets/filter_grids/common/action_delegates/chip.dart b/lib/widgets/filter_grids/common/action_delegates/chip.dart index 988e30792..3679a812c 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip.dart @@ -1,7 +1,12 @@ import 'package:aves/model/actions/chip_actions.dart'; +import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/highlight.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/vaults/vaults.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/action_mixins/vault_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; @@ -10,8 +15,26 @@ import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class ChipActionDelegate { +class ChipActionDelegate with FeedbackMixin, VaultAwareMixin { + bool isVisible( + ChipAction action, { + required CollectionFilter filter, + }) { + switch (action) { + case ChipAction.goToAlbumPage: + case ChipAction.goToCountryPage: + case ChipAction.goToTagPage: + case ChipAction.reverse: + return true; + case ChipAction.hide: + return !(filter is AlbumFilter && vaults.isVault(filter.album)); + case ChipAction.lockVault: + return (filter is AlbumFilter && vaults.isVault(filter.album) && !vaults.isLocked(filter.album)); + } + } + void onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) { + reportService.log('$action'); switch (action) { case ChipAction.goToAlbumPage: _goTo(context, filter, AlbumListPage.routeName, (context) => const AlbumListPage()); @@ -28,8 +51,10 @@ class ChipActionDelegate { case ChipAction.hide: _hide(context, filter); break; - default: - break; + case ChipAction.lockVault: + if (filter is AlbumFilter) { + lockFilters({filter}); + } } } @@ -40,8 +65,7 @@ class ChipActionDelegate { WidgetBuilder pageBuilder, ) { context.read().set(filter); - Navigator.pushAndRemoveUntil( - context, + Navigator.maybeOf(context)?.pushAndRemoveUntil( MaterialPageRoute( settings: RouteSettings(name: routeName), builder: pageBuilder, @@ -58,11 +82,12 @@ class ChipActionDelegate { actions: [ const CancelButton(), TextButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () => Navigator.maybeOf(context)?.pop(true), child: Text(context.l10n.hideButtonLabel), ), ], ), + routeSettings: const RouteSettings(name: AvesDialog.confirmationRouteName), ); if (confirmed == null || !confirmed) return; 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 0a01f5ff1..50e7dde04 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -11,11 +11,13 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/model/source/enums/view.dart'; +import 'package:aves/services/common/services.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; +import 'package:aves/widgets/common/action_mixins/vault_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/search/route.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; @@ -32,7 +34,7 @@ import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; -abstract class ChipSetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { +abstract class ChipSetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin, VaultAwareMixin { Iterable> get allItems; ChipSortFactor get sortFactor; @@ -87,6 +89,7 @@ abstract class ChipSetActionDelegate with FeedbackMi case ChipSetAction.toggleTitleSearch: return !useTvLayout && !isSelecting; case ChipSetAction.createAlbum: + case ChipSetAction.createVault: return false; // browsing or selecting case ChipSetAction.map: @@ -94,19 +97,21 @@ abstract class ChipSetActionDelegate with FeedbackMi case ChipSetAction.stats: return isMain; // selecting (single/multiple filters) - case ChipSetAction.delete: - return false; case ChipSetAction.hide: return isMain; case ChipSetAction.pin: return !hasSelection || !settings.pinnedFilters.containsAll(selectedFilters); case ChipSetAction.unpin: return hasSelection && settings.pinnedFilters.containsAll(selectedFilters); - // selecting (single filter) - case ChipSetAction.rename: + case ChipSetAction.delete: + case ChipSetAction.lockVault: return false; + // selecting (single filter) case ChipSetAction.setCover: return isMain; + case ChipSetAction.rename: + case ChipSetAction.configureVault: + return false; } } @@ -130,6 +135,7 @@ abstract class ChipSetActionDelegate with FeedbackMi case ChipSetAction.search: case ChipSetAction.toggleTitleSearch: case ChipSetAction.createAlbum: + case ChipSetAction.createVault: return true; // browsing or selecting case ChipSetAction.map: @@ -141,15 +147,18 @@ abstract class ChipSetActionDelegate with FeedbackMi case ChipSetAction.hide: case ChipSetAction.pin: case ChipSetAction.unpin: + case ChipSetAction.lockVault: return hasSelection; // selecting (single filter) case ChipSetAction.rename: case ChipSetAction.setCover: + case ChipSetAction.configureVault: return selectedItemCount == 1; } } void onActionSelected(BuildContext context, Set filters, ChipSetAction action) { + reportService.log('$action'); switch (action) { // general case ChipSetAction.configureView: @@ -172,6 +181,7 @@ abstract class ChipSetActionDelegate with FeedbackMi context.read().toggle(); break; case ChipSetAction.createAlbum: + case ChipSetAction.createVault: break; // browsing or selecting case ChipSetAction.map: @@ -184,8 +194,6 @@ abstract class ChipSetActionDelegate with FeedbackMi _goToStats(context, filters); break; // selecting (single/multiple filters) - case ChipSetAction.delete: - break; case ChipSetAction.hide: _hide(context, filters); break; @@ -197,16 +205,22 @@ abstract class ChipSetActionDelegate with FeedbackMi settings.pinnedFilters = settings.pinnedFilters..removeAll(filters); _browse(context); break; - // selecting (single filter) - case ChipSetAction.rename: + case ChipSetAction.delete: + case ChipSetAction.lockVault: break; + // selecting (single filter) case ChipSetAction.setCover: _setCover(context, filters.first); break; + case ChipSetAction.rename: + case ChipSetAction.configureVault: + break; } } - void _browse(BuildContext context) => context.read>>().browse(); + void _browse(BuildContext context) { + context.read>?>()?.browse(); + } Iterable _selectedEntries(BuildContext context, Set filters) { final source = context.read(); @@ -233,6 +247,7 @@ abstract class ChipSetActionDelegate with FeedbackMi tileExtentController: extentController, ); }, + routeSettings: const RouteSettings(name: TileViewDialog.routeName), ); // wait for the dialog to hide as applying the change may block the UI await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); @@ -248,8 +263,7 @@ abstract class ChipSetActionDelegate with FeedbackMi source: context.read(), fixedSelection: _selectedEntries(context, filters).where((entry) => entry.hasGps).toList(), ); - await Navigator.push( - context, + await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: MapPage.routeName), builder: (context) => MapPage(collection: mapCollection), @@ -259,8 +273,7 @@ abstract class ChipSetActionDelegate with FeedbackMi } void _goToSlideshow(BuildContext context, Set filters) { - Navigator.push( - context, + Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: SlideshowPage.routeName), builder: (context) { @@ -276,8 +289,7 @@ abstract class ChipSetActionDelegate with FeedbackMi } void _goToStats(BuildContext context, Set filters) { - Navigator.push( - context, + Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: StatsPage.routeName), builder: (context) { @@ -291,8 +303,7 @@ abstract class ChipSetActionDelegate with FeedbackMi } void _goToSearch(BuildContext context) { - Navigator.push( - context, + Navigator.maybeOf(context)?.push( SearchPageRoute( delegate: CollectionSearchDelegate( searchFieldLabel: context.l10n.searchCollectionFieldHint, @@ -310,11 +321,12 @@ abstract class ChipSetActionDelegate with FeedbackMi actions: [ const CancelButton(), TextButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () => Navigator.maybeOf(context)?.pop(true), child: Text(context.l10n.hideButtonLabel), ), ], ), + routeSettings: const RouteSettings(name: AvesDialog.confirmationRouteName), ); if (confirmed == null || !confirmed) return; @@ -324,6 +336,8 @@ abstract class ChipSetActionDelegate with FeedbackMi } void _setCover(BuildContext context, T filter) async { + if (!await unlockFilter(context, filter)) return; + final existingCover = covers.of(filter); final entryId = existingCover?.item1; final customEntry = entryId != null ? context.read().visibleEntries.firstWhereOrNull((entry) => entry.id == entryId) : null; @@ -335,6 +349,7 @@ abstract class ChipSetActionDelegate with FeedbackMi customPackage: existingCover?.item2, customColor: existingCover?.item3, ), + routeSettings: const RouteSettings(name: CoverSelectionDialog.routeName), ); if (selectedCover == null) return; diff --git a/lib/widgets/filter_grids/common/app_bar.dart b/lib/widgets/filter_grids/common/app_bar.dart index e425690f4..bf4c56959 100644 --- a/lib/widgets/filter_grids/common/app_bar.dart +++ b/lib/widgets/filter_grids/common/app_bar.dart @@ -19,6 +19,7 @@ import 'package:aves/widgets/common/search/route.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; import 'package:aves/widgets/filter_grids/common/query_bar.dart'; import 'package:aves/widgets/search/search_delegate.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; @@ -51,7 +52,7 @@ class FilterGridAppBar> createState() => _FilterGridAppBarState(); - static PopupMenuItem toMenuItem(BuildContext context, ChipSetAction action, {required bool enabled}) { + static PopupMenuEntry toMenuItem(BuildContext context, ChipSetAction action, {required bool enabled}) { late Widget child; switch (action) { case ChipSetAction.toggleTitleSearch: @@ -286,7 +287,7 @@ class _FilterGridAppBarState _buildButtonIcon( @@ -326,15 +327,20 @@ class _FilterGridAppBarState !browsingQuickActions.contains(v)); final selectionMenuActions = ChipSetActions.selection.where((v) => !selectionQuickActions.contains(v)); - final contextualMenuItems = (isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map( - (action) => FilterGridAppBar.toMenuItem(context, action, enabled: canApply(action)), - ); + final contextualMenuActions = (isSelecting ? selectionMenuActions : browsingMenuActions).where((v) => v == null || isVisible(v)).toList(); + if (contextualMenuActions.isNotEmpty && contextualMenuActions.first == null) contextualMenuActions.removeAt(0); + if (contextualMenuActions.isNotEmpty && contextualMenuActions.last == null) contextualMenuActions.removeLast(); return [ ...generalMenuItems, - if (contextualMenuItems.isNotEmpty) ...[ + if (contextualMenuActions.isNotEmpty) ...[ const PopupMenuDivider(), - ...contextualMenuItems, + ...contextualMenuActions.map( + (action) { + if (action == null) return const PopupMenuDivider(); + return FilterGridAppBar.toMenuItem(context, action, enabled: canApply(action)); + }, + ), ], ]; }, @@ -422,8 +428,7 @@ class _FilterGridAppBarState extends StatelessWidget { final T filter; final double extent, thumbnailExtent; - final bool showText, pinned; + final bool showText, pinned, locked; final String? banner; final FilterCallback? onTap; final HeroType heroType; @@ -34,6 +36,7 @@ class CoveredFilterChip extends StatelessWidget { double? thumbnailExtent, this.showText = true, this.pinned = false, + required this.locked, this.banner, this.onTap, this.heroType = HeroType.onTap, @@ -98,17 +101,18 @@ class CoveredFilterChip extends StatelessWidget { } Widget _buildChip(BuildContext context, CollectionSource source) { - final entry = source.coverEntry(filter); + final _filter = filter; + final entry = _filter is AlbumFilter && vaults.isLocked(_filter.album) ? null : source.coverEntry(_filter); final titlePadding = min(4.0, extent / 32); Key? chipKey; - if (filter is AlbumFilter) { + if (_filter is AlbumFilter) { // when we asynchronously fetch installed app names, // album filters themselves do not change, but decoration derived from it does chipKey = ValueKey(androidFileUtils.areAppNamesReadyNotifier.value); } return AvesFilterChip( key: chipKey, - filter: filter, + filter: _filter, showText: showText, showGenericIcon: false, decoration: AvesFilterDecoration( @@ -128,10 +132,10 @@ class CoveredFilterChip extends StatelessWidget { }, child: entry == null ? StreamBuilder?>( - stream: covers.colorChangeStream.where((event) => event == null || event.contains(filter)), + stream: covers.colorChangeStream.where((event) => event == null || event.contains(_filter)), builder: (context, snapshot) { return FutureBuilder( - future: filter.color(context), + future: _filter.color(context), builder: (context, snapshot) { final color = snapshot.data; const neutral = Colors.white; @@ -159,7 +163,7 @@ class CoveredFilterChip extends StatelessWidget { radius: radius(extent), ), banner: banner, - details: showText ? _buildDetails(context, source, filter) : null, + details: showText ? _buildDetails(context, source, _filter) : null, padding: titlePadding, heroType: heroType, onTap: onTap, @@ -199,8 +203,18 @@ class CoveredFilterChip extends StatelessWidget { size: iconSize, ), ), + if (filter is AlbumFilter && vaults.isVault(filter.album)) + AnimatedPadding( + padding: EdgeInsetsDirectional.only(end: padding), + duration: Durations.chipDecorationAnimation, + child: Icon( + AIcons.locked, + color: _detailColor(context), + size: iconSize, + ), + ), Text( - numberFormat.format(source.count(filter)), + locked ? Constants.overlayUnknown : numberFormat.format(source.count(filter)), style: TextStyle( color: _detailColor(context), fontSize: fontSize, diff --git a/lib/widgets/filter_grids/common/enums.dart b/lib/widgets/filter_grids/common/enums.dart index dca3f4a68..0113ecfca 100644 --- a/lib/widgets/filter_grids/common/enums.dart +++ b/lib/widgets/filter_grids/common/enums.dart @@ -2,7 +2,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; -enum AlbumImportance { newAlbum, pinned, special, apps, regular } +enum AlbumImportance { newAlbum, pinned, special, apps, vaults, regular } extension ExtraAlbumImportance on AlbumImportance { String getText(BuildContext context) { @@ -15,6 +15,8 @@ extension ExtraAlbumImportance on AlbumImportance { return context.l10n.albumTierSpecial; case AlbumImportance.apps: return context.l10n.albumTierApps; + case AlbumImportance.vaults: + return context.l10n.albumTierVaults; case AlbumImportance.regular: return context.l10n.albumTierRegular; } @@ -30,6 +32,8 @@ extension ExtraAlbumImportance on AlbumImportance { return AIcons.important; case AlbumImportance.apps: return AIcons.app; + case AlbumImportance.vaults: + return AIcons.locked; case AlbumImportance.regular: return AIcons.album; } diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 7f0e61c5b..9e6ca391a 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -5,13 +5,16 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/highlight.dart'; import 'package:aves/model/query.dart'; import 'package:aves/model/selection.dart'; +import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/behaviour/pop/double_back.dart'; import 'package:aves/widgets/common/behaviour/pop/scope.dart'; import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; @@ -116,7 +119,7 @@ class FilterGridPage extends StatelessWidget { if (useTvLayout) { final canNavigate = context.select, bool>((v) => v.value.canNavigate); - return Scaffold( + return AvesScaffold( body: canNavigate ? Row( children: [ @@ -147,7 +150,7 @@ class FilterGridPage extends StatelessWidget { _draggableScrollBarEventStreamController.add(notification.event); return false; }, - child: Scaffold( + child: AvesScaffold( body: body, drawer: canNavigate ? const AppDrawer() : null, bottomNavigationBar: showBottomNavigationBar @@ -344,57 +347,63 @@ class _FilterGridContentState extends State<_FilterG extent: thumbnailExtent, child: FilterListDetailsTheme( extent: thumbnailExtent, - child: 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, + child: AnimatedBuilder( + animation: vaults, + builder: (context, child) { + return SectionedFilterListLayoutProvider( + sections: visibleSections, + showHeaders: widget.showHeaders, + selectable: widget.selectable, tileLayout: tileLayout, - banner: _getFilterBanner(context, gridItem.filter), - heroType: widget.heroType, - ); - if (!settings.useTvLayout) return tile; + 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; - } + 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, + ), + ); }, - 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, - ), + tileAnimationDelay: tileAnimationDelay, + coverRatioResolver: (item) { + final coverEntry = source.coverEntry(item.filter) ?? item.entry; + return coverEntry?.displayAspectRatio ?? 1; + }, + child: child!, ); }, - tileAnimationDelay: tileAnimationDelay, - coverRatioResolver: (item) { - final coverEntry = source.coverEntry(item.filter) ?? item.entry; - return coverEntry?.displayAspectRatio ?? 1; - }, - child: child!, + child: child, ), ), ); @@ -413,7 +422,7 @@ class _FilterGridContentState extends State<_FilterG selectable: widget.selectable, emptyBuilder: widget.emptyBuilder, bannerBuilder: _getFilterBanner, - scrollController: PrimaryScrollController.of(context)!, + scrollController: PrimaryScrollController.of(context), tileLayout: tileLayout, ), ); @@ -520,13 +529,15 @@ class _FilterSectionedContentState extends State<_Fi Future _checkInitHighlight() async { final highlightInfo = context.read(); final filter = highlightInfo.clear(); - if (filter is T) { - final gridItem = visibleSections.values.expand((list) => list).firstWhereOrNull((gridItem) => gridItem.filter == filter); - if (gridItem != null) { - await Future.delayed(Durations.highlightScrollInitDelay); - highlightInfo.trackItem(gridItem, highlightItem: filter); - } - } + if (filter is! T) return; + + final item = visibleSections.values.expand((list) => list).firstWhereOrNull((gridItem) => gridItem.filter == filter); + if (item == null) return; + + await Future.delayed(Durations.highlightScrollInitDelay); + + final animate = context.read().accessibilityAnimations.animate; + highlightInfo.trackItem(item, animate: animate, highlightItem: filter); } } @@ -616,7 +627,7 @@ class _FilterScrollView extends StatelessWidget { @override Widget build(BuildContext context) { final scrollView = _buildScrollView(context); - return _buildDraggableScrollView(scrollView); + return settings.useTvLayout ? scrollView : _buildDraggableScrollView(scrollView); } Widget _buildDraggableScrollView(ScrollView scrollView) { diff --git a/lib/widgets/filter_grids/common/filter_tile.dart b/lib/widgets/filter_grids/common/filter_tile.dart index 26981ca5b..5322e6f53 100644 --- a/lib/widgets/filter_grids/common/filter_tile.dart +++ b/lib/widgets/filter_grids/common/filter_tile.dart @@ -1,10 +1,14 @@ import 'package:aves/app_mode.dart'; +import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/selection.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/action_mixins/vault_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/scaling.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; @@ -35,7 +39,7 @@ class InteractiveFilterTile extends StatefulWidget { State> createState() => _InteractiveFilterTileState(); } -class _InteractiveFilterTileState extends State> { +class _InteractiveFilterTileState extends State> with FeedbackMixin, VaultAwareMixin { HeroType? _heroTypeOverride; FilterGridItem get gridItem => widget.gridItem; @@ -46,7 +50,9 @@ class _InteractiveFilterTileState extends State onTap() async { + if (!await unlockFilter(context, filter)) return; + final appMode = context.read?>()?.value; switch (appMode) { case AppMode.main: @@ -61,7 +67,7 @@ class _InteractiveFilterTileState extends State(context, filter); + Navigator.maybeOf(context)?.pop(filter); break; case AppMode.pickMediaInternal: case AppMode.screenSaver: @@ -96,8 +102,7 @@ class _InteractiveFilterTileState extends State CollectionPage( @@ -136,6 +141,7 @@ class FilterTile extends StatelessWidget { Widget build(BuildContext context) { final filter = gridItem.filter; final pinned = settings.pinnedFilters.contains(filter); + final locked = filter is AlbumFilter && vaults.isLocked(filter.album); final onChipTap = onTap != null ? (filter) => onTap?.call() : null; switch (tileLayout) { @@ -152,6 +158,7 @@ class FilterTile extends StatelessWidget { thumbnailExtent: thumbnailExtent, showText: true, pinned: pinned, + locked: locked, banner: banner, onTap: onChipTap, heroType: heroType, @@ -171,6 +178,7 @@ class FilterTile extends StatelessWidget { extent: chipExtent, thumbnailExtent: thumbnailExtent, showText: false, + locked: locked, banner: banner, onTap: onChipTap, heroType: heroType, @@ -180,6 +188,7 @@ class FilterTile extends StatelessWidget { child: FilterListDetails( gridItem: gridItem, pinned: pinned, + locked: locked, ), ), ], diff --git a/lib/widgets/filter_grids/common/list_details.dart b/lib/widgets/filter_grids/common/list_details.dart index 8bce14199..84c11a2e3 100644 --- a/lib/widgets/filter_grids/common/list_details.dart +++ b/lib/widgets/filter_grids/common/list_details.dart @@ -16,7 +16,7 @@ import 'package:provider/provider.dart'; class FilterListDetails extends StatelessWidget { final FilterGridItem gridItem; - final bool pinned; + final bool pinned, locked; T get filter => gridItem.filter; @@ -26,6 +26,7 @@ class FilterListDetails extends StatelessWidget { super.key, required this.gridItem, required this.pinned, + required this.locked, }); @override @@ -72,9 +73,11 @@ class FilterListDetails extends StatelessWidget { // otherwise the leading icon will be low-res scaled up/down textScaleFactor: 1, ), - const SizedBox(height: FilterListDetailsTheme.titleDetailPadding), - if (detailsTheme.showDate) _buildDateRow(context, detailsTheme, hasTitleLeading), - if (detailsTheme.showCount) _buildCountRow(context, detailsTheme, hasTitleLeading), + if (!locked) ...[ + const SizedBox(height: FilterListDetailsTheme.titleDetailPadding), + if (detailsTheme.showDate) _buildDateRow(context, detailsTheme, hasTitleLeading), + if (detailsTheme.showCount) _buildCountRow(context, detailsTheme, hasTitleLeading), + ], ], ), ); diff --git a/lib/widgets/filter_grids/common/section_keys.dart b/lib/widgets/filter_grids/common/section_keys.dart index 5f700f9c1..a93185c39 100644 --- a/lib/widgets/filter_grids/common/section_keys.dart +++ b/lib/widgets/filter_grids/common/section_keys.dart @@ -32,6 +32,8 @@ class AlbumImportanceSectionKey extends ChipSectionKey { factory AlbumImportanceSectionKey.apps(BuildContext context) => AlbumImportanceSectionKey._private(context, AlbumImportance.apps); + factory AlbumImportanceSectionKey.vault(BuildContext context) => AlbumImportanceSectionKey._private(context, AlbumImportance.vaults); + factory AlbumImportanceSectionKey.regular(BuildContext context) => AlbumImportanceSectionKey._private(context, AlbumImportance.regular); @override diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 91b391e53..be46586ce 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -16,7 +16,9 @@ import 'package:aves/services/global_search.dart'; import 'package:aves/services/intent_service.dart'; import 'package:aves/services/widget_service.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; 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'; @@ -87,7 +89,7 @@ class _HomePageState extends State { } @override - Widget build(BuildContext context) => const Scaffold(); + Widget build(BuildContext context) => const AvesScaffold(); Future _setup() async { final stopwatch = Stopwatch()..start(); @@ -95,10 +97,7 @@ class _HomePageState extends State { // do not check whether permission was granted, because some app stores // hide in some countries apps that force quit on permission denial await [ - Permission.storage, - // for media access on Android >=13 - Permission.photos, - Permission.videos, + ...Constants.storagePermissions, // to access media with unredacted metadata with scoped storage (Android >=10) Permission.accessMediaLocation, ].request(); @@ -239,8 +238,7 @@ class _HomePageState extends State { // `pushReplacement` is not enough in some edge cases // e.g. when opening the viewer in `view` mode should replace a viewer in `main` mode - unawaited(Navigator.pushAndRemoveUntil( - context, + unawaited(Navigator.maybeOf(context)?.pushAndRemoveUntil( await _getRedirectRoute(appMode), (route) => false, )); diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index ed5108c40..cb708fb97 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -19,6 +19,7 @@ import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/menu.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; @@ -59,7 +60,7 @@ class MapPage extends StatelessWidget { // as the map can be stacked on top of other pages // that catch highlight events and will not let it bubble up return HighlightInfoProvider( - child: Scaffold( + child: AvesScaffold( body: SafeArea( left: false, top: false, @@ -456,8 +457,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin void _goToViewer(AvesEntry? initialEntry) { if (initialEntry == null) return; - Navigator.push( - context, + Navigator.maybeOf(context)?.push( TransparentMaterialPageRoute( settings: const RouteSettings(name: EntryViewerPage.routeName), pageBuilder: (context, a, sa) { @@ -476,8 +476,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin final isMainMode = context.read>().value == AppMode.main; if (!isMainMode) return; - Navigator.pushAndRemoveUntil( - context, + Navigator.maybeOf(context)?.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( @@ -518,7 +517,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin Offset tapLocalPosition, WidgetBuilder markerBuilder, ) async { - final overlay = Overlay.of(context)!.context.findRenderObject() as RenderBox; + final overlay = Overlay.of(context).context.findRenderObject() as RenderBox; const touchArea = Size(kMinInteractiveDimension, kMinInteractiveDimension); final selectedAction = await showMenu( context: context, diff --git a/lib/widgets/navigation/drawer/app_drawer.dart b/lib/widgets/navigation/drawer/app_drawer.dart index 0a3acf5e6..d2ab4380f 100644 --- a/lib/widgets/navigation/drawer/app_drawer.dart +++ b/lib/widgets/navigation/drawer/app_drawer.dart @@ -105,14 +105,12 @@ class _AppDrawerState extends State { Widget _buildHeader(BuildContext context) { Future goTo(String routeName, WidgetBuilder pageBuilder) async { - Navigator.pop(context); + Navigator.maybeOf(context)?.pop(); await Future.delayed(Durations.drawerTransitionAnimation); - await Navigator.push( - context, - MaterialPageRoute( - settings: RouteSettings(name: routeName), - builder: pageBuilder, - )); + await Navigator.maybeOf(context)?.push(MaterialPageRoute( + settings: RouteSettings(name: routeName), + builder: pageBuilder, + )); } return Container( @@ -205,7 +203,7 @@ class _AppDrawerState extends State { stream: source.eventBus.on(), builder: (context, snapshot) { final albums = settings.drawerAlbumBookmarks ?? AppDrawer.getDefaultAlbums(context); - if (albums.isEmpty) return const SizedBox.shrink(); + if (albums.isEmpty) return const SizedBox(); return Column( children: [ const Divider(), diff --git a/lib/widgets/navigation/drawer/collection_nav_tile.dart b/lib/widgets/navigation/drawer/collection_nav_tile.dart index 8a8787d0e..c3e3afdda 100644 --- a/lib/widgets/navigation/drawer/collection_nav_tile.dart +++ b/lib/widgets/navigation/drawer/collection_nav_tile.dart @@ -57,9 +57,8 @@ class CollectionNavTile extends StatelessWidget { } void _goToCollection(BuildContext context) { - Navigator.pop(context); - Navigator.pushAndRemoveUntil( - context, + Navigator.maybeOf(context)?.pop(); + Navigator.maybeOf(context)?.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( diff --git a/lib/widgets/navigation/drawer/page_nav_tile.dart b/lib/widgets/navigation/drawer/page_nav_tile.dart index 5d334770b..1e77dbdd7 100644 --- a/lib/widgets/navigation/drawer/page_nav_tile.dart +++ b/lib/widgets/navigation/drawer/page_nav_tile.dart @@ -47,16 +47,15 @@ class PageNavTile extends StatelessWidget { ) : null, onTap: () { - Navigator.pop(context); + Navigator.maybeOf(context)?.pop(); final route = routeBuilder(context, routeName); if (topLevel) { - Navigator.pushAndRemoveUntil( - context, + Navigator.maybeOf(context)?.pushAndRemoveUntil( route, (route) => false, ); } else { - Navigator.push(context, route); + Navigator.maybeOf(context)?.push(route); } }, selected: context.currentRouteName == routeName, diff --git a/lib/widgets/navigation/nav_bar/nav_bar.dart b/lib/widgets/navigation/nav_bar/nav_bar.dart index 33018e6fa..899fc89d5 100644 --- a/lib/widgets/navigation/nav_bar/nav_bar.dart +++ b/lib/widgets/navigation/nav_bar/nav_bar.dart @@ -135,8 +135,7 @@ class _AppBottomNavBarState extends State { void _goTo(BuildContext context, List items, int index) { final item = items[index]; final routeName = item.route; - Navigator.pushAndRemoveUntil( - context, + Navigator.maybeOf(context)?.pushAndRemoveUntil( MaterialPageRoute( settings: RouteSettings(name: routeName), builder: (context) { diff --git a/lib/widgets/navigation/tv_rail.dart b/lib/widgets/navigation/tv_rail.dart index cac68e950..7f92fe603 100644 --- a/lib/widgets/navigation/tv_rail.dart +++ b/lib/widgets/navigation/tv_rail.dart @@ -249,16 +249,14 @@ class _TvRailState extends State { ); void _goTo(String routeName) { - Navigator.pushAndRemoveUntil( - context, + Navigator.maybeOf(context)?.pushAndRemoveUntil( PageNavTile.routeBuilder(context, routeName), (route) => false, ); } void _goToCollection(BuildContext context, CollectionFilter? filter) { - Navigator.pushAndRemoveUntil( - context, + Navigator.maybeOf(context)?.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index ab6f21f13..bd6705ca9 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -19,6 +19,8 @@ import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/action_mixins/vault_aware.dart'; import 'package:aves/widgets/common/basic/tv_edge_focus.dart'; import 'package:aves/widgets/common/expandable_filter_row.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -30,7 +32,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class CollectionSearchDelegate extends AvesSearchDelegate { +class CollectionSearchDelegate extends AvesSearchDelegate with FeedbackMixin, VaultAwareMixin { final CollectionSource source; final CollectionLens? parentCollection; final ValueNotifier _expandedSectionNotifier = ValueNotifier(null); @@ -285,12 +287,14 @@ class CollectionSearchDelegate extends AvesSearchDelegate { return cleanQuery.isNotEmpty ? QueryFilter(cleanQuery, colorful: colorful) : null; } - void _select(BuildContext context, CollectionFilter? filter) { + Future _select(BuildContext context, CollectionFilter? filter) async { if (filter == null) { goBack(context); return; } + if (!await unlockFilter(context, filter)) return; + if (settings.saveSearchHistory) { final history = settings.searchHistory ..remove(filter) @@ -320,8 +324,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate { void _jumpToCollectionPage(BuildContext context, Set filters) { clean(); - Navigator.pushAndRemoveUntil( - context, + Navigator.maybeOf(context)?.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( diff --git a/lib/widgets/settings/app_export/selection_dialog.dart b/lib/widgets/settings/app_export/selection_dialog.dart index 167dfbca7..b0577410a 100644 --- a/lib/widgets/settings/app_export/selection_dialog.dart +++ b/lib/widgets/settings/app_export/selection_dialog.dart @@ -56,7 +56,7 @@ class _AppExportItemSelectionDialogState extends State Navigator.pop(context, _selectedItems), + onPressed: _selectedItems.isEmpty ? null : () => Navigator.maybeOf(context)?.pop(_selectedItems), child: Text(context.l10n.applyButtonLabel), ), ], diff --git a/lib/widgets/settings/common/quick_actions/editor_page.dart b/lib/widgets/settings/common/quick_actions/editor_page.dart index c2b238d69..4736dd4d4 100644 --- a/lib/widgets/settings/common/quick_actions/editor_page.dart +++ b/lib/widgets/settings/common/quick_actions/editor_page.dart @@ -4,6 +4,7 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/basic/scaffold.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'; @@ -37,7 +38,7 @@ class QuickActionEditorPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( + return AvesScaffold( appBar: AppBar( title: Text(title), ), diff --git a/lib/widgets/settings/common/tiles.dart b/lib/widgets/settings/common/tiles.dart index 8af0698ae..a1892e6fc 100644 --- a/lib/widgets/settings/common/tiles.dart +++ b/lib/widgets/settings/common/tiles.dart @@ -24,8 +24,7 @@ class SettingsSubPageTile extends StatelessWidget { return ListTile( title: Text(title), onTap: () { - Navigator.push( - context, + Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: RouteSettings(name: routeName), builder: builder, diff --git a/lib/widgets/settings/display/display.dart b/lib/widgets/settings/display/display.dart index 222319665..851eaa134 100644 --- a/lib/widgets/settings/display/display.dart +++ b/lib/widgets/settings/display/display.dart @@ -125,7 +125,7 @@ class SettingsTileDisplayForceTvLayout extends SettingsTile { actions: [ const CancelButton(), TextButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () => Navigator.maybeOf(context)?.pop(true), child: Text(l10n.applyButtonLabel), ), ], diff --git a/lib/widgets/settings/home_widget_settings_page.dart b/lib/widgets/settings/home_widget_settings_page.dart index fe5f81068..a0c1341c6 100644 --- a/lib/widgets/settings/home_widget_settings_page.dart +++ b/lib/widgets/settings/home_widget_settings_page.dart @@ -8,6 +8,7 @@ import 'package:aves/services/widget_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/identity/buttons/outlined_button.dart'; @@ -67,7 +68,7 @@ class _HomeWidgetSettingsPageState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; - return Scaffold( + return AvesScaffold( appBar: AppBar( title: Text(l10n.settingsWidgetPageTitle), ), diff --git a/lib/widgets/settings/language/locale_selection_page.dart b/lib/widgets/settings/language/locale_selection_page.dart index 7a8621a7b..8c95015f5 100644 --- a/lib/widgets/settings/language/locale_selection_page.dart +++ b/lib/widgets/settings/language/locale_selection_page.dart @@ -4,6 +4,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/basic/query_bar.dart'; import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/language/locale_tile.dart'; import 'package:collection/collection.dart'; @@ -31,7 +32,7 @@ class _LocaleSelectionPageState extends State { @override Widget build(BuildContext context) { final useTvLayout = settings.useTvLayout; - return Scaffold( + return AvesScaffold( appBar: AppBar( automaticallyImplyLeading: !useTvLayout, title: Text(context.l10n.settingsLanguagePageTitle), @@ -60,7 +61,7 @@ class _LocaleSelectionPageState extends State { key: Key(value.toString()), value: value, groupValue: _selectedValue, - onChanged: (v) => Navigator.pop(context, v), + onChanged: (v) => Navigator.maybeOf(context)?.pop(v), reselectable: true, title: Text( title, diff --git a/lib/widgets/settings/language/locale_tile.dart b/lib/widgets/settings/language/locale_tile.dart index 8ea04d67b..e56081eb6 100644 --- a/lib/widgets/settings/language/locale_tile.dart +++ b/lib/widgets/settings/language/locale_tile.dart @@ -25,8 +25,7 @@ class LocaleTile extends StatelessWidget { }, ), onTap: () async { - final value = await Navigator.push( - context, + final value = await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: LocaleSelectionPage.routeName), builder: (context) => const LocaleSelectionPage(), diff --git a/lib/widgets/settings/language/locales.dart b/lib/widgets/settings/language/locales.dart index 0fcc896de..7d3889f64 100644 --- a/lib/widgets/settings/language/locales.dart +++ b/lib/widgets/settings/language/locales.dart @@ -8,6 +8,7 @@ class SupportedLocales { 'el': 'Ελληνικά', 'en': 'English', 'es': 'Español (México)', + 'eu': 'Euskara', 'fr': 'Français', 'id': 'Bahasa Indonesia', 'it': 'Italiano', diff --git a/lib/widgets/settings/navigation/confirmation_dialogs.dart b/lib/widgets/settings/navigation/confirmation_dialogs.dart index 95172f23b..2e80643e3 100644 --- a/lib/widgets/settings/navigation/confirmation_dialogs.dart +++ b/lib/widgets/settings/navigation/confirmation_dialogs.dart @@ -1,4 +1,5 @@ import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:flutter/material.dart'; @@ -11,7 +12,7 @@ class ConfirmationDialogPage extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - return Scaffold( + return AvesScaffold( appBar: AppBar( title: Text(l10n.settingsConfirmationDialogTitle), ), @@ -38,6 +39,12 @@ class ConfirmationDialogPage extends StatelessWidget { onChanged: (v) => settings.confirmAfterMoveToBin = v, title: l10n.settingsConfirmationAfterMoveToBinItems, ), + const Divider(height: 32), + SettingsSwitchListTile( + selector: (context, s) => s.confirmCreateVault, + onChanged: (v) => settings.confirmCreateVault = v, + title: l10n.settingsConfirmationVaultDataLoss, + ), ]), ), ); diff --git a/lib/widgets/settings/navigation/drawer.dart b/lib/widgets/settings/navigation/drawer.dart index e0df890b5..6cb73edfd 100644 --- a/lib/widgets/settings/navigation/drawer.dart +++ b/lib/widgets/settings/navigation/drawer.dart @@ -1,6 +1,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/search/page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; @@ -91,7 +92,7 @@ class _NavigationDrawerEditorPageState extends State return DefaultTabController( length: tabs.length, - child: Scaffold( + child: AvesScaffold( appBar: AppBar( automaticallyImplyLeading: !settings.useTvLayout, title: Text(l10n.settingsNavigationDrawerEditorPageTitle), diff --git a/lib/widgets/settings/privacy/access_grants_page.dart b/lib/widgets/settings/privacy/access_grants_page.dart index 19612b2dd..0edb980fc 100644 --- a/lib/widgets/settings/privacy/access_grants_page.dart +++ b/lib/widgets/settings/privacy/access_grants_page.dart @@ -1,5 +1,6 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/empty.dart'; import 'package:flutter/material.dart'; @@ -27,7 +28,7 @@ class _StorageAccessPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return AvesScaffold( appBar: AppBar( title: Text(context.l10n.settingsStorageAccessPageTitle), ), @@ -39,7 +40,7 @@ class _StorageAccessPageState extends State { return Text(snapshot.error.toString()); } if (snapshot.connectionState != ConnectionState.done && _lastPaths == null) { - return const SizedBox.shrink(); + return const SizedBox(); } _lastPaths = snapshot.data!..sort(); if (_lastPaths!.isEmpty) { diff --git a/lib/widgets/settings/privacy/file_picker/file_picker_page.dart b/lib/widgets/settings/privacy/file_picker/file_picker_page.dart index c0786f6e3..c20b29b25 100644 --- a/lib/widgets/settings/privacy/file_picker/file_picker_page.dart +++ b/lib/widgets/settings/privacy/file_picker/file_picker_page.dart @@ -7,6 +7,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/basic/menu.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons/outlined_button.dart'; import 'package:aves/widgets/common/identity/empty.dart'; @@ -64,7 +65,7 @@ class _FilePickerPageState extends State { setState(() {}); return SynchronousFuture(false); }, - child: Scaffold( + child: AvesScaffold( appBar: AppBar( title: Text(_getTitle(context)), actions: [ @@ -129,7 +130,7 @@ class _FilePickerPageState extends State { padding: const EdgeInsets.all(8), child: AvesOutlinedButton( label: l10n.filePickerUseThisFolder, - onPressed: () => Navigator.pop(context, currentDirectoryPath), + onPressed: () => Navigator.maybeOf(context)?.pop(currentDirectoryPath), ), ), ], @@ -165,7 +166,7 @@ class _FilePickerPageState extends State { leading: Icon(icon), title: Text(v.getDescription(context)), onTap: () async { - Navigator.pop(context); + Navigator.maybeOf(context)?.pop(); await Future.delayed(Durations.drawerTransitionAnimation); _goTo(v.path); setState(() {}); diff --git a/lib/widgets/settings/privacy/hidden_items_page.dart b/lib/widgets/settings/privacy/hidden_items_page.dart index 9a58f892e..62a63b537 100644 --- a/lib/widgets/settings/privacy/hidden_items_page.dart +++ b/lib/widgets/settings/privacy/hidden_items_page.dart @@ -3,6 +3,7 @@ import 'package:aves/model/filters/path.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; 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/outlined_button.dart'; @@ -34,7 +35,7 @@ class HiddenItemsPage extends StatelessWidget { return DefaultTabController( length: tabs.length, - child: Scaffold( + child: AvesScaffold( appBar: AppBar( automaticallyImplyLeading: !settings.useTvLayout, title: Text(l10n.settingsHiddenItemsPageTitle), @@ -144,8 +145,7 @@ class _HiddenPaths extends StatelessWidget { icon: const Icon(AIcons.add), label: context.l10n.addPathTooltip, onPressed: () async { - final path = await Navigator.push( - context, + final path = await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: FilePickerPage.routeName), builder: (context) => const FilePickerPage(), diff --git a/lib/widgets/settings/privacy/privacy.dart b/lib/widgets/settings/privacy/privacy.dart index 31f002afe..38088cd6d 100644 --- a/lib/widgets/settings/privacy/privacy.dart +++ b/lib/widgets/settings/privacy/privacy.dart @@ -3,10 +3,15 @@ import 'dart:async'; import 'package:aves/app_flavor.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/privacy/access_grants_page.dart'; @@ -92,7 +97,41 @@ class SettingsTilePrivacyEnableBin extends SettingsTile { @override Widget build(BuildContext context) => SettingsSwitchListTile( selector: (context, s) => s.enableBin, - onChanged: (v) { + onChanged: (v) async { + final l10n = context.l10n; + if (!v) { + if (vaults.all.any((v) => v.useBin)) { + await showDialog( + context: context, + builder: (context) => AvesDialog( + content: Text(l10n.vaultBinUsageDialogMessage), + actions: const [OkButton()], + ), + ); + return; + } + + final source = context.read(); + final trashedEntries = source.trashedEntries; + if (trashedEntries.isNotEmpty) { + if (!await showConfirmationDialog( + context: context, + message: l10n.settingsDisablingBinWarningDialogMessage, + confirmationButtonLabel: l10n.applyButtonLabel, + )) return; + + // delete forever trashed items + await EntrySetActionDelegate().doDelete( + context: context, + entries: trashedEntries, + enableBin: false, + ); + + // in case of failure or cancellation + if (source.trashedEntries.isNotEmpty) return; + } + } + settings.enableBin = v; if (!v) { settings.searchHistory = []; diff --git a/lib/widgets/settings/screen_saver_settings_page.dart b/lib/widgets/settings/screen_saver_settings_page.dart index 5e252e4c7..8d7902244 100644 --- a/lib/widgets/settings/screen_saver_settings_page.dart +++ b/lib/widgets/settings/screen_saver_settings_page.dart @@ -3,6 +3,7 @@ import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/slideshow_video_playback.dart'; import 'package:aves/model/settings/enums/viewer_transition.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/collection_tile.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; @@ -17,7 +18,7 @@ class ScreenSaverSettingsPage extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - return Scaffold( + return AvesScaffold( appBar: AppBar( title: Text(l10n.settingsScreenSaverPageTitle), ), diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index 21f480fb4..166ee11ed 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -13,6 +13,7 @@ import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/app_bar/app_bar_title.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/menu.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/behaviour/pop/scope.dart'; import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -74,7 +75,7 @@ class _SettingsPageState extends State with FeedbackMixin { final appBarTitle = Text(context.l10n.settingsPageTitle); if (settings.useTvLayout) { - return Scaffold( + return AvesScaffold( body: AvesPopScope( handlers: const [TvNavigationPopHandler.pop], child: Row( @@ -96,49 +97,8 @@ class _SettingsPageState extends State with FeedbackMixin { primary: false, ), ), - Expanded( - child: ValueListenableBuilder( - valueListenable: _tvSelectedIndexNotifier, - builder: (context, selectedIndex, child) { - final rail = NavigationRail( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - extended: true, - destinations: sections - .map((section) => NavigationRailDestination( - icon: section.icon(context), - label: Text(section.title(context)), - )) - .toList(), - selectedIndex: selectedIndex, - onDestinationSelected: (index) => _tvSelectedIndexNotifier.value = index, - minExtendedWidth: TvRail.minExtendedWidth, - ); - return LayoutBuilder( - builder: (context, constraints) { - return Row( - children: [ - SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight(child: rail), - ), - ), - Expanded( - child: MediaQuery.removePadding( - context: context, - removeLeft: !context.isRtl, - removeRight: context.isRtl, - child: _SettingsSectionBody( - loader: Future.value(sections[selectedIndex].tiles(context)), - ), - ), - ), - ], - ); - }, - ); - }, - ), + const Expanded( + child: _TvRail(), ), ], ), @@ -148,7 +108,7 @@ class _SettingsPageState extends State with FeedbackMixin { ), ); } else { - return Scaffold( + return AvesScaffold( appBar: AppBar( title: InteractiveAppBarTitle( onTap: () => _goToSearch(context), @@ -284,8 +244,7 @@ class _SettingsPageState extends State with FeedbackMixin { } void _goToSearch(BuildContext context) { - Navigator.push( - context, + Navigator.maybeOf(context)?.push( SearchPageRoute( delegate: SettingsSearchDelegate( searchFieldLabel: context.l10n.settingsSearchFieldLabel, @@ -359,3 +318,68 @@ class _SettingsSectionBody extends StatelessWidget { ); } } + +class _TvRail extends StatefulWidget { + const _TvRail(); + + @override + State<_TvRail> createState() => _TvRailState(); +} + +class _TvRailState extends State<_TvRail> { + final ValueNotifier _indexNotifier = ValueNotifier(0); + + @override + void dispose() { + _indexNotifier.dispose(); + super.dispose(); + } + + static final List sections = _SettingsPageState.sections; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: _indexNotifier, + builder: (context, selectedIndex, child) { + final rail = NavigationRail( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + extended: true, + destinations: sections + .map((section) => NavigationRailDestination( + icon: section.icon(context), + label: Text(section.title(context)), + )) + .toList(), + selectedIndex: selectedIndex, + onDestinationSelected: (index) => _indexNotifier.value = index, + minExtendedWidth: TvRail.minExtendedWidth, + ); + return LayoutBuilder( + builder: (context, constraints) { + return Row( + children: [ + SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight(child: rail), + ), + ), + Expanded( + child: MediaQuery.removePadding( + context: context, + removeLeft: !context.isRtl, + removeRight: context.isRtl, + child: _SettingsSectionBody( + loader: Future.value(sections[selectedIndex].tiles(context)), + ), + ), + ), + ], + ); + }, + ); + }, + ); + } +} diff --git a/lib/widgets/settings/thumbnails/collection_actions_editor_page.dart b/lib/widgets/settings/thumbnails/collection_actions_editor_page.dart index 24d3e0d38..82e7495b2 100644 --- a/lib/widgets/settings/thumbnails/collection_actions_editor_page.dart +++ b/lib/widgets/settings/thumbnails/collection_actions_editor_page.dart @@ -1,5 +1,6 @@ import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart'; import 'package:flutter/material.dart'; @@ -43,7 +44,7 @@ class CollectionActionEditorPage extends StatelessWidget { return DefaultTabController( length: tabs.length, - child: Scaffold( + child: AvesScaffold( appBar: AppBar( title: Text(context.l10n.settingsCollectionQuickActionEditorPageTitle), bottom: TabBar( diff --git a/lib/widgets/settings/thumbnails/overlay.dart b/lib/widgets/settings/thumbnails/overlay.dart index 56ed0d055..b8c47950e 100644 --- a/lib/widgets/settings/thumbnails/overlay.dart +++ b/lib/widgets/settings/thumbnails/overlay.dart @@ -5,6 +5,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; @@ -22,7 +23,7 @@ class ThumbnailOverlayPage extends StatelessWidget { final iconSize = _getIconSize(context); final iconColor = _getIconColor(context); - return Scaffold( + return AvesScaffold( appBar: AppBar( automaticallyImplyLeading: !settings.useTvLayout, title: Text(context.l10n.settingsThumbnailOverlayPageTitle), diff --git a/lib/widgets/settings/video/controls.dart b/lib/widgets/settings/video/controls.dart index d7743500c..cbe4423b9 100644 --- a/lib/widgets/settings/video/controls.dart +++ b/lib/widgets/settings/video/controls.dart @@ -1,6 +1,7 @@ import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/video_controls.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:flutter/material.dart'; @@ -12,7 +13,7 @@ class VideoControlsPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( + return AvesScaffold( appBar: AppBar( title: Text(context.l10n.settingsVideoControlsPageTitle), ), diff --git a/lib/widgets/settings/video/subtitle_theme.dart b/lib/widgets/settings/video/subtitle_theme.dart index 14fb00feb..220edce07 100644 --- a/lib/widgets/settings/video/subtitle_theme.dart +++ b/lib/widgets/settings/video/subtitle_theme.dart @@ -2,6 +2,7 @@ import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/subtitle_position.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/basic/color_list_tile.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/slider_list_tile.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; @@ -16,7 +17,7 @@ class SubtitleThemePage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( + return AvesScaffold( appBar: AppBar( automaticallyImplyLeading: !settings.useTvLayout, title: Text(context.l10n.settingsSubtitleThemePageTitle), diff --git a/lib/widgets/settings/video/video_settings_page.dart b/lib/widgets/settings/video/video_settings_page.dart index f01b7bf85..534396038 100644 --- a/lib/widgets/settings/video/video_settings_page.dart +++ b/lib/widgets/settings/video/video_settings_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:aves/widgets/settings/video/video.dart'; @@ -19,7 +20,7 @@ class _VideoSettingsPageState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - return Scaffold( + return AvesScaffold( appBar: AppBar( automaticallyImplyLeading: !settings.useTvLayout, title: Text(context.l10n.settingsVideoPageTitle), diff --git a/lib/widgets/settings/viewer/overlay.dart b/lib/widgets/settings/viewer/overlay.dart index acfd88678..33110575e 100644 --- a/lib/widgets/settings/viewer/overlay.dart +++ b/lib/widgets/settings/viewer/overlay.dart @@ -1,4 +1,5 @@ import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:flutter/material.dart'; @@ -13,7 +14,7 @@ class ViewerOverlayPage extends StatelessWidget { @override Widget build(BuildContext context) { final useTvLayout = settings.useTvLayout; - return Scaffold( + return AvesScaffold( appBar: AppBar( automaticallyImplyLeading: !useTvLayout, title: Text(context.l10n.settingsViewerOverlayPageTitle), diff --git a/lib/widgets/settings/viewer/slideshow.dart b/lib/widgets/settings/viewer/slideshow.dart index 208bca58d..b2f954c23 100644 --- a/lib/widgets/settings/viewer/slideshow.dart +++ b/lib/widgets/settings/viewer/slideshow.dart @@ -2,6 +2,7 @@ import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/slideshow_video_playback.dart'; import 'package:aves/model/settings/enums/viewer_transition.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:flutter/material.dart'; @@ -14,7 +15,7 @@ class ViewerSlideshowPage extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - return Scaffold( + return AvesScaffold( appBar: AppBar( automaticallyImplyLeading: !settings.useTvLayout, title: Text(l10n.settingsViewerSlideshowPageTitle), diff --git a/lib/widgets/settings/viewer/viewer_actions_editor.dart b/lib/widgets/settings/viewer/viewer_actions_editor.dart index 8b63bf131..98e58fa27 100644 --- a/lib/widgets/settings/viewer/viewer_actions_editor.dart +++ b/lib/widgets/settings/viewer/viewer_actions_editor.dart @@ -25,6 +25,7 @@ class ViewerActionEditorPage extends StatelessWidget { EntryAction.flip, ], [ + ...EntryActions.exportInternal, EntryAction.videoCaptureFrame, EntryAction.videoToggleMute, EntryAction.videoSetSpeed, diff --git a/lib/widgets/stats/mime_donut.dart b/lib/widgets/stats/mime_donut.dart index 4aee3ade5..82066c751 100644 --- a/lib/widgets/stats/mime_donut.dart +++ b/lib/widgets/stats/mime_donut.dart @@ -42,7 +42,7 @@ class _MimeDonutState extends State with AutomaticKeepAliveClientMixi Widget build(BuildContext context) { super.build(context); - if (byMimeTypes.isEmpty) return const SizedBox.shrink(); + if (byMimeTypes.isEmpty) return const SizedBox(); final l10n = context.l10n; final locale = l10n.localeName; diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index 85af4999f..ac28daf8d 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -14,7 +14,10 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/action_mixins/vault_aware.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/tv_edge_focus.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; @@ -50,7 +53,7 @@ class StatsPage extends StatefulWidget { State createState() => _StatsPageState(); } -class _StatsPageState extends State { +class _StatsPageState extends State with FeedbackMixin, VaultAwareMixin { final Map _entryCountPerCountry = {}, _entryCountPerPlace = {}, _entryCountPerTag = {}, _entryCountPerAlbum = {}; final Map _entryCountPerRating = Map.fromEntries(List.generate(7, (i) => MapEntry(5 - i, 0))); late final ValueNotifier _isPageAnimatingNotifier; @@ -219,7 +222,7 @@ class _StatsPageState extends State { } } - return Scaffold( + return AvesScaffold( appBar: AppBar( automaticallyImplyLeading: !useTvLayout, title: Text(l10n.statsPageTitle), @@ -253,8 +256,7 @@ class _StatsPageState extends State { final totalEntryCount = entries.length; final hasMore = maxRowCount != null && entryCountMap.length > maxRowCount; final onHeaderPressed = hasMore - ? () => Navigator.push( - context, + ? () => Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: StatsTopPage.routeName), builder: (context) => StatsTopPage( @@ -319,7 +321,9 @@ class _StatsPageState extends State { ]; } - void _onFilterSelection(BuildContext context, CollectionFilter filter) { + Future _onFilterSelection(BuildContext context, CollectionFilter filter) async { + if (!await unlockFilter(context, filter)) return; + if (widget.parentCollection != null) { _applyToParentCollectionPage(context, filter); } else { @@ -334,12 +338,11 @@ class _StatsPageState extends State { // even when the target is a child of an `AnimatedList`. // Do not use `WidgetsBinding.instance.addPostFrameCallback`, // as it may not trigger if there is no subsequent build. - Future.delayed(const Duration(milliseconds: 100), () => Navigator.popUntil(context, (route) => route.settings.name == CollectionPage.routeName)); + Future.delayed(const Duration(milliseconds: 100), () => Navigator.maybeOf(context)?.popUntil((route) => route.settings.name == CollectionPage.routeName)); } void _jumpToCollectionPage(BuildContext context, CollectionFilter filter) { - Navigator.pushAndRemoveUntil( - context, + Navigator.maybeOf(context)?.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( @@ -368,7 +371,7 @@ class StatsTopPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( + return AvesScaffold( appBar: AppBar( automaticallyImplyLeading: !settings.useTvLayout, title: Text(title), diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index 1cd1d0a79..0003dc347 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -14,6 +14,7 @@ import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/enums.dart'; @@ -25,6 +26,7 @@ import 'package:aves/widgets/common/action_mixins/entry_storage.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; +import 'package:aves/widgets/common/action_mixins/vault_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart'; import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; @@ -47,7 +49,7 @@ import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; -class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin, SingleEntryEditorMixin, EntryStorageMixin { +class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin, SingleEntryEditorMixin, EntryStorageMixin, VaultAwareMixin { final AvesEntry mainEntry, pageEntry; final CollectionLens? collection; final EntryInfoActionDelegate _metadataActionDelegate = EntryInfoActionDelegate(); @@ -110,9 +112,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix return canWrite; case EntryAction.copyToClipboard: case EntryAction.open: + case EntryAction.setAs: return !settings.useTvLayout; case EntryAction.info: - case EntryAction.setAs: case EntryAction.share: return true; case EntryAction.restore: @@ -173,6 +175,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } void onActionSelected(BuildContext context, EntryAction action) { + reportService.log('$action'); final targetEntry = _getTargetEntry(context, action); switch (action) { @@ -289,11 +292,13 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } } - void quickMove(BuildContext context, String album, {required bool copy}) { + Future quickMove(BuildContext context, String album, {required bool copy}) async { + if (!await unlockAlbum(context, album)) return; + final targetEntry = _getTargetEntry(context, copy ? EntryAction.copy : EntryAction.move); if (!copy && targetEntry.directory == album) return; - doQuickMove( + await doQuickMove( context, moveType: copy ? MoveType.copy : MoveType.move, entriesByDestination: { @@ -345,6 +350,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix builder: (context) => AddShortcutDialog( defaultName: targetEntry.bestTitle ?? '', ), + routeSettings: const RouteSettings(name: AddShortcutDialog.routeName), ); if (result == null) return; @@ -377,13 +383,16 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } Future _delete(BuildContext context, AvesEntry targetEntry) async { - if (settings.enableBin && !targetEntry.trashed) { + final vault = vaults.getVault(targetEntry.directory); + final enableBin = vault?.useBin ?? settings.enableBin; + + if (enableBin && !targetEntry.trashed) { await _move(context, targetEntry, moveType: MoveType.toBin); return; } final l10n = context.l10n; - if (!await showConfirmationDialog( + if (!await showSkippableConfirmationDialog( context: context, type: ConfirmationDialog.deleteForever, message: l10n.deleteEntriesConfirmationDialogMessage(1), @@ -407,6 +416,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix final options = await showDialog( context: context, builder: (context) => ExportEntryDialog(entry: targetEntry), + routeSettings: const RouteSettings(name: ExportEntryDialog.routeName), ); if (options == null) return; @@ -443,14 +453,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix nameConflictStrategy: NameConflictStrategy.rename, ), itemCount: selectionCount, - onDone: (processed) { + onDone: (processed) async { final successOps = processed.where((e) => e.success).toSet(); final exportedOps = successOps.where((e) => !e.skipped).toSet(); final newUris = exportedOps.map((v) => v.newFields['uri'] as String?).whereNotNull().toSet(); final isMainMode = context.read>().value == AppMode.main; source.resumeMonitoring(); - source.refreshUris(newUris); + unawaited(source.refreshUris(newUris)); final l10n = context.l10n; final showAction = isMainMode && newUris.isNotEmpty @@ -460,8 +470,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix // local context may be deactivated when action is triggered after navigation final context = AvesApp.navigatorKey.currentContext; if (context != null) { - Navigator.pushAndRemoveUntil( - context, + Navigator.maybeOf(context)?.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( @@ -505,6 +514,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix final newName = await showDialog( context: context, builder: (context) => RenameEntryDialog(entry: targetEntry), + routeSettings: const RouteSettings(name: RenameEntryDialog.routeName), ); if (newName == null || newName.isEmpty || newName == targetEntry.filenameWithoutExtension) return; @@ -521,8 +531,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix bool _isMainMode(BuildContext context) => context.read>().value == AppMode.main; void _goToSourceViewer(BuildContext context, AvesEntry targetEntry) { - Navigator.push( - context, + Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: SourceViewerPage.routeName), builder: (context) => SourceViewerPage( @@ -540,8 +549,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } void _goToDebug(BuildContext context, AvesEntry targetEntry) { - Navigator.push( - context, + Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: ViewerDebugPage.routeName), builder: (context) => ViewerDebugPage(entry: targetEntry), diff --git a/lib/widgets/viewer/action/entry_info_action_delegate.dart b/lib/widgets/viewer/action/entry_info_action_delegate.dart index 48ed09938..59d1cbb74 100644 --- a/lib/widgets/viewer/action/entry_info_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_info_action_delegate.dart @@ -85,6 +85,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi } void onActionSelected(BuildContext context, AvesEntry targetEntry, CollectionLens? collection, EntryAction action) async { + await reportService.log('$action'); _eventStreamController.add(ActionStartedEvent(action)); switch (action) { // general @@ -234,11 +235,12 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi actions: [ const CancelButton(), TextButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () => Navigator.maybeOf(context)?.pop(true), child: Text(context.l10n.applyButtonLabel), ), ], ), + routeSettings: const RouteSettings(name: AvesDialog.warningRouteName), ); if (confirmed == null || !confirmed) return; @@ -262,8 +264,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi listenToSource: true, fixedSelection: baseCollection.sortedEntries.where((entry) => entry.hasGps).where((entry) => entry != targetEntry).toList(), ); - await Navigator.push( - context, + await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: MapPage.routeName), builder: (context) => MapPage( @@ -276,8 +277,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi } void _goToDebug(BuildContext context, AvesEntry targetEntry) { - Navigator.push( - context, + Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: ViewerDebugPage.routeName), builder: (context) => ViewerDebugPage(entry: targetEntry), diff --git a/lib/widgets/viewer/debug/db.dart b/lib/widgets/viewer/debug/db.dart index f74a163d0..a7a039e8e 100644 --- a/lib/widgets/viewer/debug/db.dart +++ b/lib/widgets/viewer/debug/db.dart @@ -39,7 +39,7 @@ class _DbTabState extends State { void _loadDatabase() { final id = entry.id; _dbDateLoader = metadataDb.loadDates().then((values) => values[id]); - _dbEntryLoader = metadataDb.loadEntries().then((values) => values.firstWhereOrNull((row) => row.id == id)); + _dbEntryLoader = metadataDb.loadEntriesById({id}).then((values) => values.firstOrNull); _dbMetadataLoader = metadataDb.loadCatalogMetadata().then((values) => values.firstWhereOrNull((row) => row.id == id)); _dbAddressLoader = metadataDb.loadAddresses().then((values) => values.firstWhereOrNull((row) => row.id == id)); _dbTrashDetailsLoader = metadataDb.loadAllTrashDetails().then((values) => values.firstWhereOrNull((row) => row.id == id)); diff --git a/lib/widgets/viewer/debug/debug_page.dart b/lib/widgets/viewer/debug/debug_page.dart index cf89f3ed2..42dce44c1 100644 --- a/lib/widgets/viewer/debug/debug_page.dart +++ b/lib/widgets/viewer/debug/debug_page.dart @@ -69,6 +69,7 @@ class ViewerDebugPage extends StatelessWidget { info: { 'hash': '#${shortHash(entry)}', 'id': '${entry.id}', + 'origin': '${entry.origin}', 'contentId': '${entry.contentId}', 'uri': entry.uri, 'path': entry.path ?? '', diff --git a/lib/widgets/viewer/embedded/embedded_data_opener.dart b/lib/widgets/viewer/embedded/embedded_data_opener.dart index a95a21a1a..2c92fd6ce 100644 --- a/lib/widgets/viewer/embedded/embedded_data_opener.dart +++ b/lib/widgets/viewer/embedded/embedded_data_opener.dart @@ -72,8 +72,7 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin { } void _openTempEntry(BuildContext context, AvesEntry tempEntry) { - Navigator.push( - context, + Navigator.maybeOf(context)?.push( TransparentMaterialPageRoute( settings: const RouteSettings(name: EntryViewerPage.routeName), pageBuilder: (context, a, sa) => EntryViewerPage( diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index e19a65eaf..45d1507e3 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -233,7 +233,7 @@ class _ViewerVerticalPageViewState extends State { actions: { _ShowPreviousIntent: CallbackAction(onInvoke: (intent) => _goToHorizontalPage(-1, animate: false)), _ShowNextIntent: CallbackAction(onInvoke: (intent) => _goToHorizontalPage(1, animate: false)), - _LeaveIntent: CallbackAction(onInvoke: (intent) => Navigator.pop(context)), + _LeaveIntent: CallbackAction(onInvoke: (intent) => Navigator.maybeOf(context)?.pop()), _ShowInfoIntent: CallbackAction(onInvoke: (intent) => ShowInfoPageNotification().dispatch(context)), TvShowLessInfoIntent: CallbackAction(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context)), _TvShowMoreInfoIntent: CallbackAction(onInvoke: (intent) => TvShowMoreInfoNotification().dispatch(context)), @@ -320,7 +320,7 @@ class _ViewerVerticalPageViewState extends State { await _entry.catalog(background: false, force: false, persist: true); await _entry.locate(background: false, force: false, geocoderLocale: settings.appliedLocale); } else { - Navigator.pop(context); + Navigator.maybeOf(context)?.pop(); } // needed to refresh when entry changes but the page does not (e.g. on page deletion) diff --git a/lib/widgets/viewer/entry_viewer_page.dart b/lib/widgets/viewer/entry_viewer_page.dart index a58dc6315..fc860189d 100644 --- a/lib/widgets/viewer/entry_viewer_page.dart +++ b/lib/widgets/viewer/entry_viewer_page.dart @@ -1,5 +1,6 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/viewer/controller.dart'; import 'package:aves/widgets/viewer/entry_viewer_stack.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; @@ -40,7 +41,7 @@ class _EntryViewerPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return AvesScaffold( body: ViewStateConductorProvider( child: VideoConductorProvider( child: MultiPageConductorProvider( diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 871f1175f..c157b1ae8 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -64,6 +64,7 @@ class _EntryViewerStackState extends State with EntryViewContr final AChangeNotifier _verticalScrollNotifier = AChangeNotifier(); bool _overlayInitialized = false; final ValueNotifier _overlayVisible = ValueNotifier(true); + final ValueNotifier _overlayExpandedNotifier = ValueNotifier(false); late AnimationController _overlayAnimationController; late Animation _overlayButtonScale, _overlayVideoControlScale, _overlayOpacity; late Animation _overlayTopOffset; @@ -161,8 +162,10 @@ class _EntryViewerStackState extends State with EntryViewContr cleanEntryControllers(entryNotifier.value); _videoActionDelegate.dispose(); _overlayAnimationController.dispose(); - _overlayVisible.removeListener(_onOverlayVisibleChanged); - _verticalPager.removeListener(_onVerticalPageControllerChanged); + _overlayVisible.dispose(); + _overlayExpandedNotifier.dispose(); + _verticalPager.dispose(); + _heroInfoNotifier.dispose(); WidgetsBinding.instance.removeObserver(this); _unregisterWidget(widget); super.dispose(); @@ -257,6 +260,7 @@ class _EntryViewerStackState extends State with EntryViewContr return []; case AppMode.slideshow: return [ + _buildViewerTopOverlay(availableSize), _buildSlideshowBottomOverlay(availableSize), ]; default: @@ -295,9 +299,10 @@ class _EntryViewerStackState extends State with EntryViewContr child: ViewerTopOverlay( entries: entries, index: _currentEntryIndex, - hasCollection: hasCollection, mainEntry: mainEntry, scale: _overlayButtonScale, + hasCollection: hasCollection, + expandedNotifier: _overlayExpandedNotifier, availableSize: availableSize, viewInsets: _frozenViewInsets, viewPadding: _frozenViewPadding, @@ -503,8 +508,7 @@ class _EntryViewerStackState extends State with EntryViewContr if (baseCollection == null) return; _onLeave(); - Navigator.pushAndRemoveUntil( - context, + Navigator.maybeOf(context)?.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( @@ -602,7 +606,7 @@ class _EntryViewerStackState extends State with EntryViewContr } if (Navigator.canPop(context)) { - Navigator.pop(context); + Navigator.maybeOf(context)?.pop(); } else { _leaveViewer(); } @@ -639,7 +643,7 @@ class _EntryViewerStackState extends State with EntryViewContr if (Navigator.canPop(context)) { void pop() { _onLeave(); - Navigator.pop(context); + Navigator.maybeOf(context)?.pop(); } // closing hero, with viewer as source diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index c0c465a8b..1f3d8e7e4 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -27,10 +27,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class BasicSection extends StatelessWidget { +class BasicSection extends StatefulWidget { final AvesEntry entry; final CollectionLens? collection; final EntryInfoActionDelegate actionDelegate; + final ValueNotifier isScrollingNotifier; final ValueNotifier isEditingMetadataNotifier; final FilterCallback onFilter; @@ -39,12 +40,54 @@ class BasicSection extends StatelessWidget { required this.entry, this.collection, required this.actionDelegate, + required this.isScrollingNotifier, required this.isEditingMetadataNotifier, required this.onFilter, }); + @override + State createState() => _BasicSectionState(); +} + +class _BasicSectionState extends State { + final FocusNode _chipFocusNode = FocusNode(); + + CollectionLens? get collection => widget.collection; + + EntryInfoActionDelegate get actionDelegate => widget.actionDelegate; + + @override + void initState() { + super.initState(); + _registerWidget(widget); + _onScrollingChanged(); + } + + @override + void didUpdateWidget(covariant BasicSection oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + _chipFocusNode.dispose(); + super.dispose(); + } + + void _registerWidget(BasicSection widget) { + widget.isScrollingNotifier.addListener(_onScrollingChanged); + } + + void _unregisterWidget(BasicSection widget) { + widget.isScrollingNotifier.removeListener(_onScrollingChanged); + } + @override Widget build(BuildContext context) { + final entry = widget.entry; return AnimatedBuilder( animation: entry.metadataChangeNotifier, builder: (context, child) { @@ -52,7 +95,12 @@ class BasicSection extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ _BasicInfo(entry: entry), - _buildChips(context), + Focus( + focusNode: _chipFocusNode, + skipTraversal: true, + canRequestFocus: false, + child: _buildChips(context), + ), _buildEditButtons(context), ], ); @@ -60,6 +108,7 @@ class BasicSection extends StatelessWidget { } Widget _buildChips(BuildContext context) { + final entry = widget.entry; final tags = entry.tags.toList()..sort(compareAsciiUpperCaseNatural); final album = entry.directory; final filters = { @@ -91,7 +140,7 @@ class BasicSection extends StatelessWidget { children: effectiveFilters .map((filter) => AvesFilterChip( filter: filter, - onTap: onFilter, + onTap: widget.onFilter, )) .toList(), ), @@ -101,6 +150,7 @@ class BasicSection extends StatelessWidget { } Widget _buildEditButtons(BuildContext context) { + final entry = widget.entry; final children = [ EntryAction.editRating, EntryAction.editTags, @@ -124,8 +174,9 @@ class BasicSection extends StatelessWidget { } Widget _buildEditMetadataButton(BuildContext context, EntryAction action) { + final entry = widget.entry; return ValueListenableBuilder( - valueListenable: isEditingMetadataNotifier, + valueListenable: widget.isEditingMetadataNotifier, builder: (context, editingAction, child) { final isEditing = editingAction != null; final onPressed = isEditing ? null : () => actionDelegate.onActionSelected(context, entry, collection, action); @@ -181,6 +232,16 @@ class BasicSection extends StatelessWidget { }, ); } + + void _onScrollingChanged() { + if (!widget.isScrollingNotifier.value) { + if (settings.useTvLayout) { + // using `autofocus` while scrolling seems to fail for widget built offscreen + // so we give focus to this page when the screen is no longer scrolling + _chipFocusNode.children.firstOrNull?.requestFocus(); + } + } + } } class _BasicInfo extends StatefulWidget { diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 58e56006f..48519c873 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -7,6 +7,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/tv_edge_focus.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart'; @@ -39,53 +40,20 @@ class InfoPage extends StatefulWidget { } class _InfoPageState extends State { - final FocusNode _focusNode = FocusNode(); final ScrollController _scrollController = ScrollController(); bool _scrollStartFromTop = false; static const splitScreenWidthThreshold = 600; - @override - void initState() { - super.initState(); - _registerWidget(widget); - _onScrollingChanged(); - } - - @override - void didUpdateWidget(covariant InfoPage oldWidget) { - super.didUpdateWidget(oldWidget); - _unregisterWidget(oldWidget); - _registerWidget(widget); - } - @override void dispose() { - _unregisterWidget(widget); - _focusNode.dispose(); _scrollController.dispose(); super.dispose(); } - void _registerWidget(InfoPage widget) { - widget.isScrollingNotifier.addListener(_onScrollingChanged); - } - - void _unregisterWidget(InfoPage widget) { - widget.isScrollingNotifier.removeListener(_onScrollingChanged); - } - - void _onScrollingChanged() { - if (!widget.isScrollingNotifier.value) { - // using `autofocus` while scrolling seems to fail for widget built offscreen - // so we give focus to this page when the screen is no longer scrolling - _focusNode.requestFocus(); - } - } - @override Widget build(BuildContext context) { - return Scaffold( + return AvesScaffold( body: GestureAreaProtectorStack( child: SafeArea( bottom: false, @@ -103,16 +71,13 @@ class _InfoPageState extends State { final targetEntry = pageEntry ?? mainEntry; return EmbeddedDataOpener( entry: targetEntry, - child: Focus( - focusNode: _focusNode, - child: _InfoPageContent( - collection: widget.collection, - entry: targetEntry, - isScrollingNotifier: widget.isScrollingNotifier, - scrollController: _scrollController, - split: mqWidth > splitScreenWidthThreshold, - goToViewer: _goToViewer, - ), + child: _InfoPageContent( + collection: widget.collection, + entry: targetEntry, + isScrollingNotifier: widget.isScrollingNotifier, + scrollController: _scrollController, + split: mqWidth > splitScreenWidthThreshold, + goToViewer: _goToViewer, ), ); } @@ -239,6 +204,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { entry: entry, collection: collection, actionDelegate: _actionDelegate, + isScrollingNotifier: widget.isScrollingNotifier, isEditingMetadataNotifier: _isEditingMetadataNotifier, onFilter: _onFilter, ); diff --git a/lib/widgets/viewer/info/info_search.dart b/lib/widgets/viewer/info/info_search.dart index 4fc688ead..3bdba7745 100644 --- a/lib/widgets/viewer/info/info_search.dart +++ b/lib/widgets/viewer/info/info_search.dart @@ -30,7 +30,7 @@ class InfoSearchDelegate extends SearchDelegate { icon: AnimatedIcons.menu_arrow, progress: transitionAnimation, ), - onPressed: () => Navigator.pop(context), + onPressed: () => Navigator.maybeOf(context)?.pop(), tooltip: MaterialLocalizations.of(context).backButtonTooltip, ); } diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index ae316d050..2742cd701 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -137,8 +137,7 @@ class _LocationSectionState extends State { listenToSource: true, fixedSelection: baseCollection.sortedEntries.where((entry) => entry.hasGps).toList(), ); - await Navigator.push( - context, + await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: MapPage.routeName), builder: (context) => MapPage( diff --git a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart index f49c3aee6..def7e8670 100644 --- a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart +++ b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart @@ -40,16 +40,60 @@ class MetadataDirTile extends StatelessWidget { var tags = dir.tags; if (tags.isEmpty) return const SizedBox(); + return AvesExpansionTile( + title: title, + highlightColor: getTitleColor(context, dir), + expandedNotifier: expandedDirectoryNotifier, + initiallyExpanded: initiallyExpanded, + children: [ + MetadataDirTileBody( + entry: entry, + dir: dir, + showThumbnails: showThumbnails, + ), + ], + ); + } + + static Color getTitleColor(BuildContext context, MetadataDirectory dir) { final dirName = dir.name; if (dirName == MetadataDirectory.xmpDirectory) { - return XmpDirTile( - entry: entry, - title: title, - allTags: dir.allTags, - tags: tags, - expandedNotifier: expandedDirectoryNotifier, - initiallyExpanded: initiallyExpanded, - ); + return context.select((v) => v.xmp); + } else { + final colors = context.watch(); + return dir.color ?? colors.fromBrandColor(BrandColors.get(dirName)) ?? colors.fromString(dirName); + } + } +} + +class MetadataDirTileBody extends StatelessWidget { + final AvesEntry entry; + final MetadataDirectory dir; + final bool showThumbnails; + + const MetadataDirTileBody({ + super.key, + required this.entry, + required this.dir, + this.showThumbnails = true, + }); + + @override + Widget build(BuildContext context) { + var tags = dir.tags; + + late final List children; + final dirName = dir.name; + if (dirName == MetadataDirectory.xmpDirectory) { + children = [ + Padding( + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), + child: XmpDirTileBody( + allTags: dir.allTags, + tags: tags, + ), + ), + ]; } else { Map? linkHandlers; switch (dirName) { @@ -67,25 +111,23 @@ class MetadataDirTile extends StatelessWidget { break; } - final colors = context.watch(); - return AvesExpansionTile( - title: title, - highlightColor: dir.color ?? colors.fromBrandColor(BrandColors.get(dirName)) ?? colors.fromString(dirName), - expandedNotifier: expandedDirectoryNotifier, - initiallyExpanded: initiallyExpanded, - children: [ - if (showThumbnails && dirName == MetadataDirectory.exifThumbnailDirectory) MetadataThumbnails(entry: entry), - Padding( - padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: InfoRowGroup( - info: tags, - maxValueLength: Constants.infoGroupMaxValueLength, - spanBuilders: linkHandlers, - ), + children = [ + if (showThumbnails && dirName == MetadataDirectory.exifThumbnailDirectory) MetadataThumbnails(entry: entry), + Padding( + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), + child: InfoRowGroup( + info: tags, + maxValueLength: Constants.infoGroupMaxValueLength, + spanBuilders: linkHandlers, ), - ], - ); + ), + ]; } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ); } static Map getSvgLinkHandlers(SplayTreeMap tags) { @@ -93,8 +135,7 @@ class MetadataDirTile extends StatelessWidget { 'Metadata': InfoRowGroup.linkSpanBuilder( linkText: (context) => context.l10n.viewerInfoViewXmlLinkText, onTap: (context) { - Navigator.push( - context, + Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: SourceViewerPage.routeName), builder: (context) => SourceViewerPage( diff --git a/lib/widgets/viewer/info/metadata/metadata_section.dart b/lib/widgets/viewer/info/metadata/metadata_section.dart index 5bee4740c..80e76a7ce 100644 --- a/lib/widgets/viewer/info/metadata/metadata_section.dart +++ b/lib/widgets/viewer/info/metadata/metadata_section.dart @@ -2,11 +2,14 @@ import 'dart:async'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_info.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/identity/buttons/outlined_button.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart'; +import 'package:aves/widgets/viewer/info/metadata/tv_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -79,7 +82,7 @@ class _MetadataSectionSliverState extends State { builder: (context, metadata, child) { Widget content; if (metadata.isEmpty) { - content = const SizedBox.shrink(); + content = const SizedBox(); } else { final durations = context.watch(); content = Column( @@ -92,15 +95,32 @@ class _MetadataSectionSliverState extends State { child: child, ), ), - children: [ - const SectionRow(icon: AIcons.info), - ...metadata.entries.map((kv) => MetadataDirTile( - entry: entry, - title: kv.key, - dir: kv.value, - expandedDirectoryNotifier: _expandedDirectoryNotifier, - )), - ], + children: settings.useTvLayout + ? [ + AvesOutlinedButton( + label: MaterialLocalizations.of(context).moreButtonTooltip, + onPressed: () { + Navigator.maybeOf(context)?.push( + MaterialPageRoute( + settings: const RouteSettings(name: TvMetadataPage.routeName), + builder: (context) => TvMetadataPage( + entry: entry, + metadata: metadata, + ), + ), + ); + }, + ), + ] + : [ + const SectionRow(icon: AIcons.info), + ...metadata.entries.map((kv) => MetadataDirTile( + entry: entry, + title: kv.key, + dir: kv.value, + expandedDirectoryNotifier: _expandedDirectoryNotifier, + )), + ], ), ); } diff --git a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart index 0bb79ee82..542b001eb 100644 --- a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart +++ b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart @@ -50,7 +50,7 @@ class _MetadataThumbnailsState extends State { ), ); } - return const SizedBox.shrink(); + return const SizedBox(); }); } } diff --git a/lib/widgets/viewer/info/metadata/tv_page.dart b/lib/widgets/viewer/info/metadata/tv_page.dart new file mode 100644 index 000000000..38273c361 --- /dev/null +++ b/lib/widgets/viewer/info/metadata/tv_page.dart @@ -0,0 +1,123 @@ +import 'package:aves/model/entry.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; +import 'package:aves/widgets/common/behaviour/intents.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/navigation/tv_rail.dart'; +import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart'; +import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class TvMetadataPage extends StatefulWidget { + static const routeName = '/info/metadata'; + + final AvesEntry entry; + final Map metadata; + + const TvMetadataPage({ + super.key, + required this.entry, + required this.metadata, + }); + + @override + State createState() => _TvMetadataPageState(); +} + +class _TvMetadataPageState extends State { + final ValueNotifier _railIndexNotifier = ValueNotifier(0); + final FocusNode _railFocusNode = FocusNode(); + final ScrollController _detailsScrollController = ScrollController(); + + Map get metadata => widget.metadata; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _railFocusNode.children.firstOrNull?.requestFocus(); + }); + } + + @override + void dispose() { + _railIndexNotifier.dispose(); + _railFocusNode.dispose(); + _detailsScrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AvesScaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(context.l10n.viewerInfoPageTitle), + ), + body: ValueListenableBuilder( + valueListenable: _railIndexNotifier, + builder: (context, selectedIndex, child) { + final titles = metadata.keys.toList(); + final selectedDir = metadata[titles[selectedIndex]]; + if (selectedDir == null) return const SizedBox(); + + final rail = NavigationRail( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + extended: true, + destinations: titles.mapIndexed((i, title) { + final dir = metadata[titles[i]]!; + final color = MetadataDirTile.getTitleColor(context, dir); + return NavigationRailDestination( + icon: Icon(AIcons.disc, color: color), + label: Text(title), + ); + }).toList(), + selectedIndex: selectedIndex, + onDestinationSelected: (index) => _railIndexNotifier.value = index, + minExtendedWidth: TvRail.minExtendedWidth, + ); + + return SafeArea( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(width: 16), + Focus( + focusNode: _railFocusNode, + skipTraversal: true, + canRequestFocus: false, + child: SingleChildScrollView( + child: IntrinsicHeight( + child: rail, + ), + ), + ), + Expanded( + child: FocusableActionDetector( + shortcuts: const { + SingleActivator(LogicalKeyboardKey.arrowUp): VerticalScrollIntent.up(), + SingleActivator(LogicalKeyboardKey.arrowDown): VerticalScrollIntent.down(), + }, + actions: { + VerticalScrollIntent: VerticalScrollIntentAction(scrollController: _detailsScrollController), + }, + child: SingleChildScrollView( + controller: _detailsScrollController, + padding: const EdgeInsets.all(16), + child: MetadataDirTileBody( + entry: widget.entry, + dir: selectedDir, + ), + ), + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/widgets/viewer/info/metadata/xmp_tile.dart b/lib/widgets/viewer/info/metadata/xmp_tile.dart index ff71933e6..fd3ced944 100644 --- a/lib/widgets/viewer/info/metadata/xmp_tile.dart +++ b/lib/widgets/viewer/info/metadata/xmp_tile.dart @@ -1,41 +1,27 @@ import 'dart:collection'; import 'dart:convert'; -import 'package:aves/model/entry.dart'; -import 'package:aves/theme/colors.dart'; import 'package:aves/utils/xmp_utils.dart'; -import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -class XmpDirTile extends StatefulWidget { - final AvesEntry entry; - final String title; +class XmpDirTileBody extends StatefulWidget { final SplayTreeMap allTags, tags; - final ValueNotifier? expandedNotifier; - final bool initiallyExpanded; - const XmpDirTile({ + const XmpDirTileBody({ super.key, - required this.entry, - required this.title, required this.allTags, required this.tags, - required this.expandedNotifier, - required this.initiallyExpanded, }); @override - State createState() => _XmpDirTileState(); + State createState() => _XmpDirTileBodyState(); } -class _XmpDirTileState extends State { +class _XmpDirTileBodyState extends State { late final Map _schemaRegistryPrefixes, _tags; - AvesEntry get entry => widget.entry; - static const schemaRegistryPrefixesKey = 'schemaRegistryPrefixes'; @override @@ -60,21 +46,10 @@ class _XmpDirTileState extends State { return XmpNamespace.create(_schemaRegistryPrefixes, nsPrefix, rawProps); }).toList() ..sort((a, b) => compareAsciiUpperCase(a.displayTitle, b.displayTitle)); - return AvesExpansionTile( - // title may contain parent to distinguish multiple XMP directories - title: widget.title, - highlightColor: context.select((v) => v.xmp), - expandedNotifier: widget.expandedNotifier, - initiallyExpanded: widget.initiallyExpanded, - children: [ - Padding( - padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: sections.expand((section) => section.buildNamespaceSection(context)).toList(), - ), - ), - ], + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: sections.expand((section) => section.buildNamespaceSection(context)).toList(), ); } } diff --git a/lib/widgets/viewer/overlay/details/description.dart b/lib/widgets/viewer/overlay/details/description.dart index 02a476fdf..fec7ab219 100644 --- a/lib/widgets/viewer/overlay/details/description.dart +++ b/lib/widgets/viewer/overlay/details/description.dart @@ -14,12 +14,24 @@ class OverlayDescriptionRow extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - children: [ - DecoratedIcon(AIcons.description, size: ViewerDetailOverlayContent.iconSize, shadows: ViewerDetailOverlayContent.shadows(context)), - const SizedBox(width: ViewerDetailOverlayContent.iconPadding), - Expanded(child: Text(description, strutStyle: Constants.overflowStrutStyle)), - ], + return Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: const EdgeInsetsDirectional.only(end: ViewerDetailOverlayContent.iconPadding), + child: DecoratedIcon( + AIcons.description, + size: ViewerDetailOverlayContent.iconSize, + shadows: ViewerDetailOverlayContent.shadows(context), + ), + ), + ), + TextSpan(text: description), + ], + ), + strutStyle: Constants.overflowStrutStyle, ); } } diff --git a/lib/widgets/viewer/overlay/details/details.dart b/lib/widgets/viewer/overlay/details/details.dart index 0e7be08e7..d24265ee7 100644 --- a/lib/widgets/viewer/overlay/details/details.dart +++ b/lib/widgets/viewer/overlay/details/details.dart @@ -9,6 +9,7 @@ import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/overlay/details/date.dart'; import 'package:aves/widgets/viewer/overlay/details/description.dart'; +import 'package:aves/widgets/viewer/overlay/details/expander.dart'; import 'package:aves/widgets/viewer/overlay/details/location.dart'; import 'package:aves/widgets/viewer/overlay/details/position_title.dart'; import 'package:aves/widgets/viewer/overlay/details/rating_tags.dart'; @@ -23,6 +24,7 @@ class ViewerDetailOverlay extends StatefulWidget { final int index; final bool hasCollection; final MultiPageController? multiPageController; + final ValueNotifier expandedNotifier; final Size availableSize; const ViewerDetailOverlay({ @@ -31,6 +33,7 @@ class ViewerDetailOverlay extends StatefulWidget { required this.index, required this.hasCollection, required this.multiPageController, + required this.expandedNotifier, required this.availableSize, }); @@ -102,6 +105,7 @@ class _ViewerDetailOverlayState extends State { position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null, availableWidth: widget.availableSize.width, multiPageController: multiPageController, + expandedNotifier: widget.expandedNotifier, ); return multiPageController != null @@ -123,6 +127,7 @@ class ViewerDetailOverlayContent extends StatelessWidget { final String? position; final double availableWidth; final MultiPageController? multiPageController; + final ValueNotifier expandedNotifier; static const double _interRowPadding = 2.0; static const double _subRowMinWidth = 300.0; @@ -140,6 +145,7 @@ class ViewerDetailOverlayContent extends StatelessWidget { required this.position, required this.availableWidth, required this.multiPageController, + required this.expandedNotifier, }); @override @@ -152,7 +158,11 @@ class ViewerDetailOverlayContent extends StatelessWidget { return AnimatedBuilder( animation: pageEntry.metadataChangeNotifier, builder: (context, child) { - final positionTitle = OverlayPositionTitleRow(entry: pageEntry, collectionPosition: position, multiPageController: multiPageController); + final positionTitle = OverlayPositionTitleRow( + entry: pageEntry, + collectionPosition: position, + multiPageController: multiPageController, + ); return DefaultTextStyle( style: Theme.of(context).textTheme.bodyMedium!.copyWith( shadows: shadows(context), @@ -172,7 +182,10 @@ class ViewerDetailOverlayContent extends StatelessWidget { final rows = []; if (positionTitle.isNotEmpty) { - rows.add(positionTitle); + rows.add(OverlayRowExpander( + expandedNotifier: expandedNotifier, + child: positionTitle, + )); rows.add(const SizedBox(height: _interRowPadding)); } if (twoColumns) { @@ -225,13 +238,19 @@ class ViewerDetailOverlayContent extends StatelessWidget { Widget _buildRatingTagsFullRow(BuildContext context) => _buildFullRowSwitcher( context: context, visible: pageEntry.rating != 0 || pageEntry.tags.isNotEmpty, - builder: (context) => OverlayRatingTagsRow(entry: pageEntry), + builder: (context) => OverlayRowExpander( + expandedNotifier: expandedNotifier, + child: OverlayRatingTagsRow(entry: pageEntry), + ), ); Widget _buildDescriptionFullRow(BuildContext context) => _buildFullRowSwitcher( context: context, visible: description != null, - builder: (context) => OverlayDescriptionRow(description: description!), + builder: (context) => OverlayRowExpander( + expandedNotifier: expandedNotifier, + child: OverlayDescriptionRow(description: description!), + ), ); Widget _buildShootingFullRow(BuildContext context, double subRowWidth) => _buildFullRowSwitcher( diff --git a/lib/widgets/viewer/overlay/details/expander.dart b/lib/widgets/viewer/overlay/details/expander.dart new file mode 100644 index 000000000..592546145 --- /dev/null +++ b/lib/widgets/viewer/overlay/details/expander.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class OverlayRowExpander extends StatelessWidget { + final ValueNotifier expandedNotifier; + final Widget child; + + const OverlayRowExpander({ + super.key, + required this.expandedNotifier, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: expandedNotifier, + builder: (context, expanded, child) { + final parent = DefaultTextStyle.of(context); + return DefaultTextStyle( + key: key, + style: parent.style, + textAlign: parent.textAlign, + softWrap: expanded, + overflow: parent.overflow, + maxLines: expanded ? null : 42, + textWidthBasis: parent.textWidthBasis, + child: child!, + ); + }, + child: child, + ); + } +} diff --git a/lib/widgets/viewer/overlay/details/rating_tags.dart b/lib/widgets/viewer/overlay/details/rating_tags.dart index c96f4e877..f99c222a1 100644 --- a/lib/widgets/viewer/overlay/details/rating_tags.dart +++ b/lib/widgets/viewer/overlay/details/rating_tags.dart @@ -1,13 +1,11 @@ import 'package:aves/model/entry.dart'; -import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/widgets/common/basic/text/animated_diff.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/overlay/details/details.dart'; +import 'package:collection/collection.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class OverlayRatingTagsRow extends AnimatedWidget { final AvesEntry entry; @@ -33,30 +31,31 @@ class OverlayRatingTagsRow extends AnimatedWidget { break; } - final tags = entry.tags.join(Constants.separator); + final tags = entry.tags.toList()..sort(compareAsciiUpperCaseNatural); final hasTags = tags.isNotEmpty; - final animationDuration = context.select((v) => v.textDiffAnimation); - return Row( - children: [ - AnimatedDiffText( - ratingString, - strutStyle: Constants.overflowStrutStyle, - duration: animationDuration, - ), - if (hasTags) ...[ - if (ratingString.isNotEmpty) const Text(Constants.separator), - DecoratedIcon(AIcons.tag, size: ViewerDetailOverlayContent.iconSize, shadows: ViewerDetailOverlayContent.shadows(context)), - const SizedBox(width: ViewerDetailOverlayContent.iconPadding), - Expanded( - child: AnimatedDiffText( - tags, - strutStyle: Constants.overflowStrutStyle, - duration: animationDuration, + return Text.rich( + TextSpan( + children: [ + TextSpan(text: ratingString), + if (hasTags) ...[ + if (ratingString.isNotEmpty) const TextSpan(text: Constants.separator), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: const EdgeInsetsDirectional.only(end: ViewerDetailOverlayContent.iconPadding), + child: DecoratedIcon( + AIcons.tag, + size: ViewerDetailOverlayContent.iconSize, + shadows: ViewerDetailOverlayContent.shadows(context), + ), + ), ), - ), + TextSpan(text: tags.join(Constants.separator)), + ] ], - ], + ), + strutStyle: Constants.overflowStrutStyle, ); } } diff --git a/lib/widgets/viewer/overlay/panorama.dart b/lib/widgets/viewer/overlay/panorama.dart index b62ae86fb..5f853f583 100644 --- a/lib/widgets/viewer/overlay/panorama.dart +++ b/lib/widgets/viewer/overlay/panorama.dart @@ -27,8 +27,7 @@ class PanoramaOverlay extends StatelessWidget { onPressed: () async { final info = await metadataFetchService.getPanoramaInfo(entry); if (info != null) { - unawaited(Navigator.push( - context, + unawaited(Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: PanoramaPage.routeName), builder: (context) => PanoramaPage( diff --git a/lib/widgets/viewer/overlay/slideshow_buttons.dart b/lib/widgets/viewer/overlay/slideshow_buttons.dart index d7d282d4a..ee630d502 100644 --- a/lib/widgets/viewer/overlay/slideshow_buttons.dart +++ b/lib/widgets/viewer/overlay/slideshow_buttons.dart @@ -29,6 +29,7 @@ class _SlideshowButtonsState extends State { static const List _actions = [ SlideshowAction.resume, SlideshowAction.showInCollection, + SlideshowAction.settings, ]; static const double _padding = ViewerButtonRowContent.padding; diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index 3d3e8ddaa..7021e0149 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -16,6 +16,7 @@ class ViewerTopOverlay extends StatelessWidget { final AvesEntry mainEntry; final Animation scale; final bool hasCollection; + final ValueNotifier expandedNotifier; final Size availableSize; final EdgeInsets? viewInsets, viewPadding; @@ -26,6 +27,7 @@ class ViewerTopOverlay extends StatelessWidget { required this.mainEntry, required this.scale, required this.hasCollection, + required this.expandedNotifier, required this.availableSize, required this.viewInsets, required this.viewPadding, @@ -51,23 +53,27 @@ class ViewerTopOverlay extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (showInfo) - BlurredRect( - enabled: blurred, - child: Container( - color: Themes.overlayBackgroundColor(brightness: Theme.of(context).brightness, blurred: blurred), - child: SafeArea( - bottom: false, - minimum: EdgeInsets.only( - left: viewInsetsPadding.left, - top: viewInsetsPadding.top, - right: viewInsetsPadding.right, - ), - child: ViewerDetailOverlay( - index: index, - entries: entries, - hasCollection: hasCollection, - multiPageController: multiPageController, - availableSize: availableSize, + GestureDetector( + onTap: () => expandedNotifier.value = !expandedNotifier.value, + child: BlurredRect( + enabled: blurred, + child: Container( + color: Themes.overlayBackgroundColor(brightness: Theme.of(context).brightness, blurred: blurred), + child: SafeArea( + bottom: false, + minimum: EdgeInsets.only( + left: viewInsetsPadding.left, + top: viewInsetsPadding.top, + right: viewInsetsPadding.right, + ), + child: ViewerDetailOverlay( + index: index, + entries: entries, + hasCollection: hasCollection, + multiPageController: multiPageController, + expandedNotifier: expandedNotifier, + availableSize: availableSize, + ), ), ), ), diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart index 3ce1e4c6d..a02bb9899 100644 --- a/lib/widgets/viewer/overlay/viewer_buttons.dart +++ b/lib/widgets/viewer/overlay/viewer_buttons.dart @@ -71,15 +71,16 @@ class ViewerButtons extends StatelessWidget { selector: (context, s) => s.isRotationLocked, builder: (context, s, child) { final quickActions = (trashed ? EntryActions.trashed : settings.viewerQuickActions).where(actionDelegate.isVisible).where(actionDelegate.canApply).take(availableCount - 1).toList(); - final topLevelActions = EntryActions.topLevel.where((action) => !quickActions.contains(action)).where(actionDelegate.isVisible).toList(); - final exportActions = EntryActions.export.where((action) => !quickActions.contains(action)).where(actionDelegate.isVisible).toList(); - final videoActions = EntryActions.video.where((action) => !quickActions.contains(action)).where(actionDelegate.isVisible).toList(); + List getMenuActions(List categoryActions) { + return categoryActions.where((action) => !quickActions.contains(action)).where(actionDelegate.isVisible).toList(); + } + return ViewerButtonRowContent( actionDelegate: EntryActionDelegate(mainEntry, pageEntry, collection), quickActions: quickActions, - topLevelActions: topLevelActions, - exportActions: exportActions, - videoActions: videoActions, + topLevelActions: getMenuActions(EntryActions.topLevel), + exportActions: getMenuActions(EntryActions.export), + videoActions: getMenuActions(EntryActions.video), scale: scale, mainEntry: mainEntry, pageEntry: pageEntry, diff --git a/lib/widgets/viewer/overlay/wallpaper_buttons.dart b/lib/widgets/viewer/overlay/wallpaper_buttons.dart index b11b84b77..ba894d6e1 100644 --- a/lib/widgets/viewer/overlay/wallpaper_buttons.dart +++ b/lib/widgets/viewer/overlay/wallpaper_buttons.dart @@ -59,6 +59,7 @@ class WallpaperButtons extends StatelessWidget with FeedbackMixin { final value = await showDialog>( context: context, builder: (context) => const WallpaperSettingsDialog(), + routeSettings: const RouteSettings(name: WallpaperSettingsDialog.routeName), ); if (value == null) return; diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index 69a38bfd5..ea1dbcd8f 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -7,6 +7,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; @@ -60,7 +61,7 @@ class _PanoramaPageState extends State { _onLeave(); return SynchronousFuture(true); }, - child: Scaffold( + child: AvesScaffold( body: Stack( children: [ ValueListenableBuilder( diff --git a/lib/widgets/viewer/screen_saver_page.dart b/lib/widgets/viewer/screen_saver_page.dart index 30c1064c4..61ec663e4 100644 --- a/lib/widgets/viewer/screen_saver_page.dart +++ b/lib/widgets/viewer/screen_saver_page.dart @@ -4,6 +4,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/viewer/controller.dart'; @@ -93,7 +94,7 @@ class _ScreenSaverPageState extends State with WidgetsBindingOb } } - return Scaffold( + return AvesScaffold( body: child, ); } diff --git a/lib/widgets/viewer/slideshow_page.dart b/lib/widgets/viewer/slideshow_page.dart index ca50abb53..e070459f5 100644 --- a/lib/widgets/viewer/slideshow_page.dart +++ b/lib/widgets/viewer/slideshow_page.dart @@ -1,20 +1,26 @@ import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/slideshow_actions.dart'; +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/empty.dart'; +import 'package:aves/widgets/settings/viewer/slideshow.dart'; import 'package:aves/widgets/viewer/controller.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/entry_viewer_stack.dart'; import 'package:aves_magnifier/aves_magnifier.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; class SlideshowPage extends StatefulWidget { static const routeName = '/collection/slideshow'; @@ -31,36 +37,33 @@ class SlideshowPage extends StatefulWidget { } class _SlideshowPageState extends State { - late final ViewerController _viewerController; - late final CollectionLens _slideshowCollection; + late ViewerController _viewerController; + late CollectionLens _slideshowCollection; + AvesEntry? _initialEntry; + + CollectionSource get source => widget.collection.source; @override void initState() { super.initState(); - _viewerController = ViewerController( - initialScale: ScaleLevel(ref: settings.slideshowFillScreen ? ScaleReference.covered : ScaleReference.contained), - transition: settings.slideshowTransition, - repeat: settings.slideshowRepeat, - autopilot: true, - autopilotInterval: Duration(seconds: settings.slideshowInterval), - autopilotAnimatedZoom: settings.slideshowAnimatedZoomEffect, - ); + _initViewerController(autopilot: true); _initSlideshowCollection(); + _initialEntry = _slideshowCollection.sortedEntries.firstOrNull; } @override void dispose() { - _viewerController.dispose(); + _disposeViewerController(); super.dispose(); } @override Widget build(BuildContext context) { - final entries = _slideshowCollection.sortedEntries; + final initialEntry = _initialEntry; return ListenableProvider>.value( value: ValueNotifier(AppMode.slideshow), - child: Scaffold( - body: entries.isEmpty + child: AvesScaffold( + body: initialEntry == null ? EmptyContent( icon: AIcons.image, text: context.l10n.collectionEmptyImages, @@ -75,8 +78,9 @@ class _SlideshowPageState extends State { return true; }, child: EntryViewerStack( + key: ValueKey(_viewerController), collection: _slideshowCollection, - initialEntry: entries.first, + initialEntry: initialEntry, viewerController: _viewerController, ), ), @@ -87,9 +91,21 @@ class _SlideshowPageState extends State { ); } + void _initViewerController({required bool autopilot}) { + _viewerController = ViewerController( + initialScale: ScaleLevel(ref: settings.slideshowFillScreen ? ScaleReference.covered : ScaleReference.contained), + transition: settings.slideshowTransition, + repeat: settings.slideshowRepeat, + autopilot: autopilot, + autopilotInterval: Duration(seconds: settings.slideshowInterval), + autopilotAnimatedZoom: settings.slideshowAnimatedZoomEffect, + ); + } + + void _disposeViewerController() => _viewerController.dispose(); + void _initSlideshowCollection() { - final originalCollection = widget.collection; - var entries = originalCollection.sortedEntries; + var entries = List.of(widget.collection.sortedEntries); if (settings.slideshowVideoPlayback == SlideshowVideoPlayback.skip) { entries = entries.where((entry) => !MimeFilter.video.test(entry)).toList(); } @@ -97,7 +113,7 @@ class _SlideshowPageState extends State { entries.shuffle(); } _slideshowCollection = CollectionLens( - source: originalCollection.source, + source: source, listenToSource: false, fixedSort: true, fixedSelection: entries, @@ -112,19 +128,20 @@ class _SlideshowPageState extends State { case SlideshowAction.showInCollection: _showInCollection(); break; + case SlideshowAction.settings: + _showSettings(context); + break; } } void _showInCollection() { - final entry = _viewerController.entryNotifier.value; - if (entry == null) return; + final currentEntry = _viewerController.entryNotifier.value; + if (currentEntry == null) return; - final source = _slideshowCollection.source; - final album = entry.directory; - final uri = entry.uri; + final album = currentEntry.directory; + final uri = currentEntry.uri; - Navigator.pushAndRemoveUntil( - context, + Navigator.maybeOf(context)?.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( @@ -136,6 +153,29 @@ class _SlideshowPageState extends State { (route) => false, ); } + + Tuple2 get collectionSettings => Tuple2(settings.slideshowShuffle, settings.slideshowVideoPlayback == SlideshowVideoPlayback.skip); + + Future _showSettings(BuildContext context) async { + final oldCollectionSettings = collectionSettings; + final currentEntry = _viewerController.entryNotifier.value; + + await Navigator.maybeOf(context)?.push( + MaterialPageRoute( + settings: const RouteSettings(name: ViewerSlideshowPage.routeName), + builder: (context) => const ViewerSlideshowPage(), + ), + ); + + _disposeViewerController(); + _initViewerController(autopilot: false); + if (oldCollectionSettings != collectionSettings) { + _initSlideshowCollection(); + } + final slideshowEntries = _slideshowCollection.sortedEntries; + _initialEntry = slideshowEntries.contains(currentEntry) ? currentEntry : slideshowEntries.firstOrNull; + setState(() {}); + } } class SlideshowActionNotification extends Notification { diff --git a/lib/widgets/viewer/source_viewer_page.dart b/lib/widgets/viewer/source_viewer_page.dart index 18e54ef94..a545eb2f9 100644 --- a/lib/widgets/viewer/source_viewer_page.dart +++ b/lib/widgets/viewer/source_viewer_page.dart @@ -1,4 +1,5 @@ import 'package:aves/widgets/common/aves_highlight.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; import 'package:flutter_highlight/themes/darcula.dart'; @@ -30,7 +31,7 @@ class _SourceViewerPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return AvesScaffold( appBar: AppBar( title: Text(context.l10n.sourceViewerPageTitle), ), @@ -39,7 +40,7 @@ class _SourceViewerPageState extends State { future: _loader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (!snapshot.hasData) return const SizedBox.shrink(); + if (!snapshot.hasData) return const SizedBox(); final data = snapshot.data!; final source = data.length < maxCodeSize ? data : '${data.substring(0, maxCodeSize)}\n\n*** TRUNCATED ***'; diff --git a/lib/widgets/viewer/video/controller.dart b/lib/widgets/viewer/video/controller.dart index 0e0f9f452..f450388e2 100644 --- a/lib/widgets/viewer/video/controller.dart +++ b/lib/widgets/viewer/video/controller.dart @@ -70,15 +70,16 @@ abstract class AvesVideoController { content: Text(context.l10n.videoResumeDialogMessage(formatFriendlyDuration(Duration(milliseconds: resumeTime)))), actions: [ TextButton( - onPressed: () => Navigator.pop(context), + onPressed: () => Navigator.maybeOf(context)?.pop(false), child: Text(context.l10n.videoStartOverButtonLabel), ), TextButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () => Navigator.maybeOf(context)?.pop(true), child: Text(context.l10n.videoResumeButtonLabel), ), ], ), + routeSettings: const RouteSettings(name: AvesDialog.confirmationRouteName), ); if (resume == null || !resume) return 0; return resumeTime; diff --git a/lib/widgets/viewer/video_action_delegate.dart b/lib/widgets/viewer/video_action_delegate.dart index a23e5bee0..e58571471 100644 --- a/lib/widgets/viewer/video_action_delegate.dart +++ b/lib/widgets/viewer/video_action_delegate.dart @@ -120,8 +120,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (context != null) { final source = _collection.source; final newUri = newFields['uri'] as String?; - Navigator.pushAndRemoveUntil( - context, + Navigator.maybeOf(context)?.pushAndRemoveUntil( MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( @@ -152,6 +151,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix builder: (context) => VideoStreamSelectionDialog( streams: Map.fromEntries(streams.map((stream) => MapEntry(stream, currentSelectedIndices.contains(stream.index)))), ), + routeSettings: const RouteSettings(name: VideoStreamSelectionDialog.routeName), ); if (userSelectedStreams == null || userSelectedStreams.isEmpty) return; @@ -169,6 +169,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix min: controller.minSpeed, max: controller.maxSpeed, ), + routeSettings: const RouteSettings(name: VideoSpeedDialog.routeName), ); if (newSpeed == null) return; @@ -181,8 +182,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix resumePosition = controller.currentPosition; await controller.pause(); } - await Navigator.push( - context, + await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: VideoSettingsPage.routeName), builder: (context) => const VideoSettingsPage(), diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 2257879b9..0b64cd867 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -283,7 +283,7 @@ class _EntryPageViewState extends State with SingleTickerProvider valueNotifier: valueNotifier, ), ); - Overlay.of(context)!.insert(_actionFeedbackOverlayEntry!); + Overlay.of(context).insert(_actionFeedbackOverlayEntry!); }; onScaleUpdate = (details) { move += details.focalPointDelta; @@ -336,10 +336,13 @@ class _EntryPageViewState extends State with SingleTickerProvider ], ); if (useVerticalDragGesture) { - videoChild = MagnifierGestureDetectorScope.of(context)!.copyWith( - acceptPointerEvent: MagnifierGestureRecognizer.isYPan, - child: videoChild, - ); + final scope = MagnifierGestureDetectorScope.maybeOf(context); + if (scope != null) { + videoChild = scope.copyWith( + acceptPointerEvent: MagnifierGestureRecognizer.isYPan, + child: videoChild, + ); + } } return Stack( fit: StackFit.expand, diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index bcbc78a90..a7770966e 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -164,7 +164,7 @@ class _RasterImageViewState extends State { return ValueListenableBuilder( valueListenable: _fullImageLoaded, builder: (context, fullImageLoaded, child) { - if (fullImageLoaded) return const SizedBox.shrink(); + if (fullImageLoaded) return const SizedBox(); return Center( child: AspectRatio( @@ -196,7 +196,7 @@ class _RasterImageViewState extends State { child = ValueListenableBuilder( valueListenable: _fullImageLoaded, builder: (context, fullImageLoaded, child) { - if (!fullImageLoaded) return const SizedBox.shrink(); + if (!fullImageLoaded) return const SizedBox(); return CustomPaint( painter: CheckeredPainter( diff --git a/lib/widgets/viewer/visual/vector.dart b/lib/widgets/viewer/visual/vector.dart index 64ea730fb..02736fa30 100644 --- a/lib/widgets/viewer/visual/vector.dart +++ b/lib/widgets/viewer/visual/vector.dart @@ -138,7 +138,7 @@ class _VectorImageViewState extends State { return ValueListenableBuilder( valueListenable: _fullImageLoaded, builder: (context, fullImageLoaded, child) { - if (fullImageLoaded) return const SizedBox.shrink(); + if (fullImageLoaded) return const SizedBox(); return Center( child: AspectRatio( diff --git a/lib/widgets/wallpaper_page.dart b/lib/widgets/wallpaper_page.dart index dc2773844..59371a258 100644 --- a/lib/widgets/wallpaper_page.dart +++ b/lib/widgets/wallpaper_page.dart @@ -5,6 +5,7 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/viewer/controller.dart'; import 'package:aves/widgets/viewer/entry_horizontal_pager.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; @@ -34,7 +35,7 @@ class WallpaperPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( + return AvesScaffold( body: entry != null ? ViewStateConductorProvider( child: VideoConductorProvider( diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index 8b867ab68..917a3b66d 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -7,6 +7,7 @@ import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/about/policy_page.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/basic/markdown_container.dart'; +import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/common/identity/buttons/outlined_button.dart'; @@ -49,7 +50,7 @@ class _WelcomePageState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return AvesScaffold( body: SafeArea( child: Center( child: FutureBuilder( @@ -164,7 +165,7 @@ class _WelcomePageState extends State { final canEnableErrorReporting = context.select((v) => v.canEnableErrorReporting); const contentPadding = EdgeInsets.symmetric(horizontal: 8); final switches = ConstrainedBox( - constraints: const BoxConstraints(maxWidth: MarkdownContainer.maxWidth), + constraints: const BoxConstraints(maxWidth: MarkdownContainer.mobileMaxWidth), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -222,8 +223,7 @@ class _WelcomePageState extends State { } void _goToPolicyPage() { - Navigator.push( - context, + Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: PolicyPage.routeName), builder: (context) => const PolicyPage(), diff --git a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart index e9ae6017b..76a6e1348 100644 --- a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart +++ b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart @@ -54,7 +54,7 @@ class _MagnifierGestureDetectorState extends State { ); } - final scope = MagnifierGestureDetectorScope.of(context); + final scope = MagnifierGestureDetectorScope.maybeOf(context); if (scope != null) { gestures[MagnifierGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => MagnifierGestureRecognizer( diff --git a/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart b/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart index a9b52c9fe..82da68aea 100644 --- a/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart +++ b/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart @@ -24,7 +24,7 @@ class MagnifierGestureDetectorScope extends InheritedWidget { required Widget child, }) : super(child: child); - static MagnifierGestureDetectorScope? of(BuildContext context) { + static MagnifierGestureDetectorScope? maybeOf(BuildContext context) { return context.dependOnInheritedWidgetOfExactType(); } diff --git a/plugins/aves_magnifier/pubspec.lock b/plugins/aves_magnifier/pubspec.lock index 8dbbe8b57..e07b21f96 100644 --- a/plugins/aves_magnifier/pubspec.lock +++ b/plugins/aves_magnifier/pubspec.lock @@ -5,21 +5,24 @@ packages: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" equatable: dependency: "direct main" description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted version: "2.0.5" flutter: @@ -31,42 +34,56 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted version: "6.0.5" sky_engine: @@ -78,9 +95,10 @@ packages: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=1.16.0" diff --git a/plugins/aves_magnifier/pubspec.yaml b/plugins/aves_magnifier/pubspec.yaml index 59f677714..bf57a7c34 100644 --- a/plugins/aves_magnifier/pubspec.yaml +++ b/plugins/aves_magnifier/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.18.0 <3.0.0" + sdk: ">=2.19.0 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_map/pubspec.lock b/plugins/aves_map/pubspec.lock index 9c5d491c8..bcfc9e221 100644 --- a/plugins/aves_map/pubspec.lock +++ b/plugins/aves_map/pubspec.lock @@ -5,7 +5,8 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted version: "2.10.0" aves_ui: @@ -19,42 +20,48 @@ packages: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" collection: dependency: "direct main" description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" custom_rounded_rectangle_border: dependency: "direct main" description: name: custom_rounded_rectangle_border - url: "https://pub.dartlang.org" + sha256: "57b7af53da4e8bf4afa5a8393c446e953a81c23dd309f4341cfc38d19ff6f062" + url: "https://pub.dev" source: hosted version: "0.2.0-nullsafety.0" equatable: dependency: "direct main" description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted version: "2.0.5" fluster: dependency: "direct main" description: name: fluster - url: "https://pub.dartlang.org" + sha256: "3807f5d088b7798f0416b8578498046338af98bb4fb922a70e2810b8293963f6" + url: "https://pub.dev" source: hosted version: "1.2.0" flutter: @@ -66,119 +73,144 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_map: dependency: "direct main" description: name: flutter_map - url: "https://pub.dartlang.org" + sha256: "59dfd14267b691bea55760786b47d3172d47cdcc0d79ff930746a5ad123491b8" + url: "https://pub.dev" source: hosted version: "3.1.0" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted version: "0.13.5" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted version: "4.0.2" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + url: "https://pub.dev" source: hosted version: "0.18.0" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" latlong2: dependency: "direct main" description: name: latlong2 - url: "https://pub.dartlang.org" + sha256: "408993a0e3f46e79ce1f129e4cb0386eef6d48dfa6394939ecacfbd7049154ec" + url: "https://pub.dev" source: hosted version: "0.8.1" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" lists: dependency: transitive description: name: lists - url: "https://pub.dartlang.org" + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" source: hosted version: "1.0.1" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" mgrs_dart: dependency: transitive description: name: mgrs_dart - url: "https://pub.dartlang.org" + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" source: hosted version: "2.0.0" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted version: "1.8.3" polylabel: dependency: transitive description: name: polylabel - url: "https://pub.dartlang.org" + sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" + url: "https://pub.dev" source: hosted version: "1.0.1" positioned_tap_detector_2: dependency: transitive description: name: positioned_tap_detector_2 - url: "https://pub.dartlang.org" + sha256: "52e06863ad3e1f82b058fd05054fc8c9caeeb3b47d5cea7a24bd9320746059c1" + url: "https://pub.dev" source: hosted version: "1.0.4" proj4dart: dependency: transitive description: name: proj4dart - url: "https://pub.dartlang.org" + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" source: hosted version: "2.1.0" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted version: "6.0.5" sky_engine: @@ -190,58 +222,66 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted version: "1.9.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" tuple: dependency: transitive description: name: tuple - url: "https://pub.dartlang.org" + sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" + url: "https://pub.dev" source: hosted version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" unicode: dependency: transitive description: name: unicode - url: "https://pub.dartlang.org" + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" source: hosted version: "0.3.1" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" wkt_parser: dependency: transitive description: name: wkt_parser - url: "https://pub.dartlang.org" + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" source: hosted version: "2.0.0" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_map/pubspec.yaml b/plugins/aves_map/pubspec.yaml index 9f2e682a6..267d70a7f 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.18.0 <3.0.0" + sdk: ">=2.19.0 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_platform_meta/android/build.gradle b/plugins/aves_platform_meta/android/build.gradle index 435bd2270..097b07644 100644 --- a/plugins/aves_platform_meta/android/build.gradle +++ b/plugins/aves_platform_meta/android/build.gradle @@ -2,14 +2,14 @@ group 'deckers.thibault.aves.aves_platform_meta' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '1.7.20' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' + classpath 'com.android.tools.build:gradle:7.2.2' 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 080650cd5..cb92fa5fd 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.3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip \ No newline at end of file diff --git a/plugins/aves_platform_meta/pubspec.lock b/plugins/aves_platform_meta/pubspec.lock index abb7eae45..ca1e1c6b8 100644 --- a/plugins/aves_platform_meta/pubspec.lock +++ b/plugins/aves_platform_meta/pubspec.lock @@ -5,16 +5,18 @@ packages: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" flutter: dependency: "direct main" description: flutter @@ -24,35 +26,48 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" plugin_platform_interface: dependency: "direct main" description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + url: "https://pub.dev" source: hosted version: "2.1.3" sky_engine: @@ -64,8 +79,9 @@ packages: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.19.0 <3.0.0" diff --git a/plugins/aves_platform_meta/pubspec.yaml b/plugins/aves_platform_meta/pubspec.yaml index 18c18d5a2..0cc9a2174 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.18.0 <3.0.0" + sdk: ">=2.19.0 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_report/pubspec.lock b/plugins/aves_report/pubspec.lock index 9d22b49d3..8ac10f808 100644 --- a/plugins/aves_report/pubspec.lock +++ b/plugins/aves_report/pubspec.lock @@ -5,16 +5,18 @@ packages: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" flutter: dependency: "direct main" description: flutter @@ -24,28 +26,40 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" sky_engine: @@ -57,8 +71,9 @@ packages: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.19.0 <3.0.0" diff --git a/plugins/aves_report/pubspec.yaml b/plugins/aves_report/pubspec.yaml index 61bcbea7c..ce2029d14 100644 --- a/plugins/aves_report/pubspec.yaml +++ b/plugins/aves_report/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.18.0 <3.0.0" + sdk: ">=2.19.0 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_report_console/pubspec.lock b/plugins/aves_report_console/pubspec.lock index 383a26c68..9e4ee23d3 100644 --- a/plugins/aves_report_console/pubspec.lock +++ b/plugins/aves_report_console/pubspec.lock @@ -12,16 +12,18 @@ packages: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" flutter: dependency: "direct main" description: flutter @@ -31,28 +33,40 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" sky_engine: @@ -64,8 +78,9 @@ packages: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.19.0 <3.0.0" diff --git a/plugins/aves_report_console/pubspec.yaml b/plugins/aves_report_console/pubspec.yaml index 45ec75833..2face2183 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.18.0 <3.0.0" + sdk: ">=2.19.0 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_report_crashlytics/pubspec.lock b/plugins/aves_report_crashlytics/pubspec.lock index 10253f93e..dbf3e5182 100644 --- a/plugins/aves_report_crashlytics/pubspec.lock +++ b/plugins/aves_report_crashlytics/pubspec.lock @@ -5,16 +5,18 @@ packages: dependency: transitive description: name: _flutterfire_internals - url: "https://pub.dartlang.org" + sha256: "6215ac7d00ed98300b72f45ed2b38c2ca841f9f4e6965fab33cbd591e45e4473" + url: "https://pub.dev" source: hosted - version: "1.0.12" + version: "1.0.13" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" aves_report: dependency: "direct main" description: @@ -26,72 +28,82 @@ packages: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" collection: dependency: "direct main" description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted version: "1.3.1" firebase_core: dependency: "direct main" description: name: firebase_core - url: "https://pub.dartlang.org" + sha256: be13e431c0c950f0fc66bdb67b41b8059121d7e7d8bbbc21fb59164892d561f8 + url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.5.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + sha256: "5615b30c36f55b2777d0533771deda7e5730e769e5d3cb7fda79e9bed86cfa55" + url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + sha256: "4b3a41410f3313bb95fd560aa5eb761b6ad65c185de772c72231e8b4aeed6d18" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - url: "https://pub.dartlang.org" + sha256: "6f1dc5321aa7d8bb84e8eec2ed517b0a7e7bc99a2708549c55c78359a323ecd0" + url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "3.0.12" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - url: "https://pub.dartlang.org" + sha256: "3728ad632a5209499cdfed6a1f7154982a524a076566f5a1d02af623bf46f947" + url: "https://pub.dev" source: hosted - version: "3.3.10" + version: "3.3.12" flutter: dependency: "direct main" description: flutter @@ -101,7 +113,8 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_test: @@ -118,49 +131,56 @@ packages: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.13" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" source: hosted version: "1.8.2" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + url: "https://pub.dev" source: hosted version: "2.1.3" sky_engine: @@ -172,51 +192,58 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" stack_trace: dependency: "direct main" description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.4.16" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=1.20.0" diff --git a/plugins/aves_report_crashlytics/pubspec.yaml b/plugins/aves_report_crashlytics/pubspec.yaml index 72706939d..aea210f04 100644 --- a/plugins/aves_report_crashlytics/pubspec.yaml +++ b/plugins/aves_report_crashlytics/pubspec.yaml @@ -3,7 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.18.0 <3.0.0" + sdk: ">=2.19.0 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_services/pubspec.lock b/plugins/aves_services/pubspec.lock index 0db4e95e9..184bee844 100644 --- a/plugins/aves_services/pubspec.lock +++ b/plugins/aves_services/pubspec.lock @@ -5,7 +5,8 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted version: "2.10.0" aves_map: @@ -26,42 +27,48 @@ packages: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" custom_rounded_rectangle_border: dependency: transitive description: name: custom_rounded_rectangle_border - url: "https://pub.dartlang.org" + sha256: "57b7af53da4e8bf4afa5a8393c446e953a81c23dd309f4341cfc38d19ff6f062" + url: "https://pub.dev" source: hosted version: "0.2.0-nullsafety.0" equatable: dependency: transitive description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted version: "2.0.5" fluster: dependency: transitive description: name: fluster - url: "https://pub.dartlang.org" + sha256: "3807f5d088b7798f0416b8578498046338af98bb4fb922a70e2810b8293963f6" + url: "https://pub.dev" source: hosted version: "1.2.0" flutter: @@ -73,119 +80,144 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_map: dependency: transitive description: name: flutter_map - url: "https://pub.dartlang.org" + sha256: "59dfd14267b691bea55760786b47d3172d47cdcc0d79ff930746a5ad123491b8" + url: "https://pub.dev" source: hosted version: "3.1.0" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted version: "0.13.5" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted version: "4.0.2" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + url: "https://pub.dev" source: hosted version: "0.18.0" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" latlong2: dependency: "direct main" description: name: latlong2 - url: "https://pub.dartlang.org" + sha256: "408993a0e3f46e79ce1f129e4cb0386eef6d48dfa6394939ecacfbd7049154ec" + url: "https://pub.dev" source: hosted version: "0.8.1" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" lists: dependency: transitive description: name: lists - url: "https://pub.dartlang.org" + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" source: hosted version: "1.0.1" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" mgrs_dart: dependency: transitive description: name: mgrs_dart - url: "https://pub.dartlang.org" + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" source: hosted version: "2.0.0" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted version: "1.8.3" polylabel: dependency: transitive description: name: polylabel - url: "https://pub.dartlang.org" + sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" + url: "https://pub.dev" source: hosted version: "1.0.1" positioned_tap_detector_2: dependency: transitive description: name: positioned_tap_detector_2 - url: "https://pub.dartlang.org" + sha256: "52e06863ad3e1f82b058fd05054fc8c9caeeb3b47d5cea7a24bd9320746059c1" + url: "https://pub.dev" source: hosted version: "1.0.4" proj4dart: dependency: transitive description: name: proj4dart - url: "https://pub.dartlang.org" + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" source: hosted version: "2.1.0" provider: dependency: transitive description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted version: "6.0.5" sky_engine: @@ -197,58 +229,66 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted version: "1.9.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" tuple: dependency: transitive description: name: tuple - url: "https://pub.dartlang.org" + sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" + url: "https://pub.dev" source: hosted version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" unicode: dependency: transitive description: name: unicode - url: "https://pub.dartlang.org" + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" source: hosted version: "0.3.1" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" wkt_parser: dependency: transitive description: name: wkt_parser - url: "https://pub.dartlang.org" + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" source: hosted version: "2.0.0" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_services/pubspec.yaml b/plugins/aves_services/pubspec.yaml index 4dc6cf3fe..cc949a8db 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.18.0 <3.0.0" + sdk: ">=2.19.0 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_services_google/pubspec.lock b/plugins/aves_services_google/pubspec.lock index c7845b7fd..b091065f7 100644 --- a/plugins/aves_services_google/pubspec.lock +++ b/plugins/aves_services_google/pubspec.lock @@ -5,7 +5,8 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted version: "2.10.0" aves_map: @@ -33,70 +34,80 @@ packages: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" custom_rounded_rectangle_border: dependency: transitive description: name: custom_rounded_rectangle_border - url: "https://pub.dartlang.org" + sha256: "57b7af53da4e8bf4afa5a8393c446e953a81c23dd309f4341cfc38d19ff6f062" + url: "https://pub.dev" source: hosted version: "0.2.0-nullsafety.0" device_info_plus: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" + sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95" + url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.1.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" source: hosted version: "7.0.0" equatable: dependency: transitive description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted version: "2.0.5" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted version: "6.1.4" fluster: dependency: transitive description: name: fluster - url: "https://pub.dartlang.org" + sha256: "3807f5d088b7798f0416b8578498046338af98bb4fb922a70e2810b8293963f6" + url: "https://pub.dev" source: hosted version: "1.2.0" flutter: @@ -108,21 +119,24 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_map: dependency: transitive description: name: flutter_map - url: "https://pub.dartlang.org" + sha256: "59dfd14267b691bea55760786b47d3172d47cdcc0d79ff930746a5ad123491b8" + url: "https://pub.dev" source: hosted version: "3.1.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b" + url: "https://pub.dev" source: hosted version: "2.0.7" flutter_web_plugins: @@ -134,154 +148,176 @@ packages: dependency: "direct main" description: name: google_api_availability - url: "https://pub.dartlang.org" + sha256: a1f58c5213abae549fb1e1451bc68eb499da7033039f77bc289fd6faca384295 + url: "https://pub.dev" source: hosted version: "3.0.1" google_maps_flutter: dependency: "direct main" description: name: google_maps_flutter - url: "https://pub.dartlang.org" + sha256: "0c6b72b4b1e0f6204973e2b40868a75fe6380725d498f215cd7e35ed920d1c57" + url: "https://pub.dev" source: hosted version: "2.2.3" google_maps_flutter_android: dependency: "direct main" description: name: google_maps_flutter_android - url: "https://pub.dartlang.org" + sha256: f238a04c378df6fbe7d84a3ea19362e9db19ea1717d06deffa9edaa17973e916 + url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" google_maps_flutter_ios: dependency: transitive description: name: google_maps_flutter_ios - url: "https://pub.dartlang.org" + sha256: "33bbca8d4148ed373251ea2ec2344fdc63009926b6d6be71a0854fd42409b1ba" + url: "https://pub.dev" source: hosted version: "2.1.13" google_maps_flutter_platform_interface: dependency: "direct main" description: name: google_maps_flutter_platform_interface - url: "https://pub.dartlang.org" + sha256: "0967430c25240836b794d42336bd4c61f0e78e9fd33d1365fa9316bb36b6b410" + url: "https://pub.dev" source: hosted version: "2.2.5" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted version: "0.13.5" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted version: "4.0.2" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + url: "https://pub.dev" source: hosted version: "0.18.0" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" latlong2: dependency: "direct main" description: name: latlong2 - url: "https://pub.dartlang.org" + sha256: "408993a0e3f46e79ce1f129e4cb0386eef6d48dfa6394939ecacfbd7049154ec" + url: "https://pub.dev" source: hosted version: "0.8.1" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" lists: dependency: transitive description: name: lists - url: "https://pub.dartlang.org" + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" source: hosted version: "1.0.1" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" mgrs_dart: dependency: transitive description: name: mgrs_dart - url: "https://pub.dartlang.org" + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" source: hosted version: "2.0.0" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted version: "1.8.3" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + url: "https://pub.dev" source: hosted version: "2.1.3" polylabel: dependency: transitive description: name: polylabel - url: "https://pub.dartlang.org" + sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" + url: "https://pub.dev" source: hosted version: "1.0.1" positioned_tap_detector_2: dependency: transitive description: name: positioned_tap_detector_2 - url: "https://pub.dartlang.org" + sha256: "52e06863ad3e1f82b058fd05054fc8c9caeeb3b47d5cea7a24bd9320746059c1" + url: "https://pub.dev" source: hosted version: "1.0.4" proj4dart: dependency: transitive description: name: proj4dart - url: "https://pub.dartlang.org" + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" source: hosted version: "2.1.0" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted version: "6.0.5" sky_engine: @@ -293,72 +329,82 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted version: "1.9.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" tuple: dependency: transitive description: name: tuple - url: "https://pub.dartlang.org" + sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" + url: "https://pub.dev" source: hosted version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" unicode: dependency: transitive description: name: unicode - url: "https://pub.dartlang.org" + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" source: hosted version: "0.3.1" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + sha256: "5cdbe09a75b5f4517adf213c68aaf53ffa162fadf54ba16f663f94f3d2664a56" + url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "4.1.1" wkt_parser: dependency: transitive description: name: wkt_parser - url: "https://pub.dartlang.org" + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" source: hosted version: "2.0.0" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_services_google/pubspec.yaml b/plugins/aves_services_google/pubspec.yaml index aaa5aa74b..e9f15853f 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.18.0 <3.0.0" + sdk: ">=2.19.0 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_services_huawei/pubspec.lock b/plugins/aves_services_huawei/pubspec.lock index 12005e2fc..7b801dfc8 100644 --- a/plugins/aves_services_huawei/pubspec.lock +++ b/plugins/aves_services_huawei/pubspec.lock @@ -5,7 +5,8 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted version: "2.10.0" aves_map: @@ -40,42 +41,48 @@ packages: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" custom_rounded_rectangle_border: dependency: transitive description: name: custom_rounded_rectangle_border - url: "https://pub.dartlang.org" + sha256: "57b7af53da4e8bf4afa5a8393c446e953a81c23dd309f4341cfc38d19ff6f062" + url: "https://pub.dev" source: hosted version: "0.2.0-nullsafety.0" equatable: dependency: transitive description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted version: "2.0.5" fluster: dependency: transitive description: name: fluster - url: "https://pub.dartlang.org" + sha256: "3807f5d088b7798f0416b8578498046338af98bb4fb922a70e2810b8293963f6" + url: "https://pub.dev" source: hosted version: "1.2.0" flutter: @@ -87,140 +94,168 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_map: dependency: transitive description: name: flutter_map - url: "https://pub.dartlang.org" + sha256: "59dfd14267b691bea55760786b47d3172d47cdcc0d79ff930746a5ad123491b8" + url: "https://pub.dev" source: hosted version: "3.1.0" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted version: "0.13.5" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted version: "4.0.2" huawei_hmsavailability: dependency: "direct main" description: name: huawei_hmsavailability - url: "https://pub.dartlang.org" + sha256: ab7128852e4188fda1e052cc44d8f881061e79fdd568edd18981347906369653 + url: "https://pub.dev" source: hosted version: "6.6.0+300" huawei_map: dependency: "direct main" description: name: huawei_map - url: "https://pub.dartlang.org" + sha256: "8438cfa448711b6727cf4433ffc52bc20e36fb20105608703c644a6287b96f38" + url: "https://pub.dev" source: hosted version: "6.5.0+301" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + url: "https://pub.dev" source: hosted version: "0.18.0" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" latlong2: dependency: "direct main" description: name: latlong2 - url: "https://pub.dartlang.org" + sha256: "408993a0e3f46e79ce1f129e4cb0386eef6d48dfa6394939ecacfbd7049154ec" + url: "https://pub.dev" source: hosted version: "0.8.1" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" lists: dependency: transitive description: name: lists - url: "https://pub.dartlang.org" + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" source: hosted version: "1.0.1" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" mgrs_dart: dependency: transitive description: name: mgrs_dart - url: "https://pub.dartlang.org" + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" source: hosted version: "2.0.0" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted version: "1.8.3" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + url: "https://pub.dev" source: hosted version: "2.1.3" polylabel: dependency: transitive description: name: polylabel - url: "https://pub.dartlang.org" + sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" + url: "https://pub.dev" source: hosted version: "1.0.1" positioned_tap_detector_2: dependency: transitive description: name: positioned_tap_detector_2 - url: "https://pub.dartlang.org" + sha256: "52e06863ad3e1f82b058fd05054fc8c9caeeb3b47d5cea7a24bd9320746059c1" + url: "https://pub.dev" source: hosted version: "1.0.4" proj4dart: dependency: transitive description: name: proj4dart - url: "https://pub.dartlang.org" + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" source: hosted version: "2.1.0" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted version: "6.0.5" sky_engine: @@ -232,65 +267,74 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted version: "1.9.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" tuple: dependency: transitive description: name: tuple - url: "https://pub.dartlang.org" + sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" + url: "https://pub.dev" source: hosted version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" unicode: dependency: transitive description: name: unicode - url: "https://pub.dartlang.org" + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" source: hosted version: "0.3.1" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" wkt_parser: dependency: transitive description: name: wkt_parser - url: "https://pub.dartlang.org" + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" source: hosted version: "2.0.0" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_services_huawei/pubspec.yaml b/plugins/aves_services_huawei/pubspec.yaml index 86bf3f125..c50e4c239 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.18.0 <3.0.0" + sdk: ">=2.19.0 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_services_none/pubspec.lock b/plugins/aves_services_none/pubspec.lock index 0c8dfed63..c43870e1d 100644 --- a/plugins/aves_services_none/pubspec.lock +++ b/plugins/aves_services_none/pubspec.lock @@ -5,7 +5,8 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted version: "2.10.0" aves_map: @@ -33,42 +34,48 @@ packages: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" custom_rounded_rectangle_border: dependency: transitive description: name: custom_rounded_rectangle_border - url: "https://pub.dartlang.org" + sha256: "57b7af53da4e8bf4afa5a8393c446e953a81c23dd309f4341cfc38d19ff6f062" + url: "https://pub.dev" source: hosted version: "0.2.0-nullsafety.0" equatable: dependency: transitive description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted version: "2.0.5" fluster: dependency: transitive description: name: fluster - url: "https://pub.dartlang.org" + sha256: "3807f5d088b7798f0416b8578498046338af98bb4fb922a70e2810b8293963f6" + url: "https://pub.dev" source: hosted version: "1.2.0" flutter: @@ -80,119 +87,144 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_map: dependency: transitive description: name: flutter_map - url: "https://pub.dartlang.org" + sha256: "59dfd14267b691bea55760786b47d3172d47cdcc0d79ff930746a5ad123491b8" + url: "https://pub.dev" source: hosted version: "3.1.0" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted version: "0.13.5" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted version: "4.0.2" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + url: "https://pub.dev" source: hosted version: "0.18.0" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" latlong2: dependency: "direct main" description: name: latlong2 - url: "https://pub.dartlang.org" + sha256: "408993a0e3f46e79ce1f129e4cb0386eef6d48dfa6394939ecacfbd7049154ec" + url: "https://pub.dev" source: hosted version: "0.8.1" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" lists: dependency: transitive description: name: lists - url: "https://pub.dartlang.org" + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" source: hosted version: "1.0.1" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" mgrs_dart: dependency: transitive description: name: mgrs_dart - url: "https://pub.dartlang.org" + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" source: hosted version: "2.0.0" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted version: "1.8.3" polylabel: dependency: transitive description: name: polylabel - url: "https://pub.dartlang.org" + sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" + url: "https://pub.dev" source: hosted version: "1.0.1" positioned_tap_detector_2: dependency: transitive description: name: positioned_tap_detector_2 - url: "https://pub.dartlang.org" + sha256: "52e06863ad3e1f82b058fd05054fc8c9caeeb3b47d5cea7a24bd9320746059c1" + url: "https://pub.dev" source: hosted version: "1.0.4" proj4dart: dependency: transitive description: name: proj4dart - url: "https://pub.dartlang.org" + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" source: hosted version: "2.1.0" provider: dependency: transitive description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted version: "6.0.5" sky_engine: @@ -204,58 +236,66 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted version: "1.9.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" tuple: dependency: transitive description: name: tuple - url: "https://pub.dartlang.org" + sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" + url: "https://pub.dev" source: hosted version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" unicode: dependency: transitive description: name: unicode - url: "https://pub.dartlang.org" + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" source: hosted version: "0.3.1" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" wkt_parser: dependency: transitive description: name: wkt_parser - url: "https://pub.dartlang.org" + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" source: hosted version: "2.0.0" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.3.0" diff --git a/plugins/aves_services_none/pubspec.yaml b/plugins/aves_services_none/pubspec.yaml index 9869ff9a3..a2b172f39 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.18.0 <3.0.0" + sdk: ">=2.19.0 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_ui/pubspec.lock b/plugins/aves_ui/pubspec.lock index 9d22b49d3..8ac10f808 100644 --- a/plugins/aves_ui/pubspec.lock +++ b/plugins/aves_ui/pubspec.lock @@ -5,16 +5,18 @@ packages: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" flutter: dependency: "direct main" description: flutter @@ -24,28 +26,40 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" sky_engine: @@ -57,8 +71,9 @@ packages: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.19.0 <3.0.0" diff --git a/plugins/aves_ui/pubspec.yaml b/plugins/aves_ui/pubspec.yaml index b7a7e9643..c39d0df10 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.18.0 <3.0.0" + sdk: ">=2.19.0 <3.0.0" dependencies: flutter: diff --git a/pubspec.lock b/pubspec.lock index 1c7170c2e..6c8596cb0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,44 +5,50 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" + url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "52.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - url: "https://pub.dartlang.org" + sha256: "6215ac7d00ed98300b72f45ed2b38c2ca841f9f4e6965fab33cbd591e45e4473" + url: "https://pub.dev" source: hosted - version: "1.0.12" + version: "1.0.13" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 + url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "5.4.0" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + sha256: "80e5141fafcb3361653ce308776cfd7d45e6e9fbb429e14eec571382c0c5fecb" + url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.2" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" aves_magnifier: dependency: "direct main" description: @@ -96,28 +102,40 @@ packages: dependency: transitive description: name: barcode - url: "https://pub.dartlang.org" + sha256: "52570564684bbb0240a9f1fdb6bad12adc5e0540103c1c96d6dd550bd928b1c9" + url: "https://pub.dev" source: hosted version: "2.2.3" + bidi: + dependency: transitive + description: + name: bidi + sha256: dc00274c7edabae2ab30c676e736ea1eb0b1b7a1b436cb5fe372e431ccb39ab0 + url: "https://pub.dev" + source: hosted + version: "2.0.6" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" charts_common: dependency: transitive description: name: charts_common - url: "https://pub.dartlang.org" + sha256: "7b8922f9b0d9b134122756a787dab1c3946ae4f3fc5022ff323ba0014998ea02" + url: "https://pub.dev" source: hosted version: "0.12.0" charts_flutter: @@ -133,112 +151,128 @@ packages: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" collection: dependency: "direct main" description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" connectivity_plus: dependency: "direct main" description: name: connectivity_plus - url: "https://pub.dartlang.org" + sha256: "8875e8ed511a49f030e313656154e4bbbcef18d68dfd32eb853fac10bce48e96" + url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.2.4" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted version: "3.1.1" country_code: dependency: "direct main" description: name: country_code - url: "https://pub.dartlang.org" + sha256: f69ccd5163b1ca43011be9632e33ebe7ffac65e49ce2afcd3e3e5228af5d91fc + url: "https://pub.dev" source: hosted version: "1.0.0" coverage: dependency: transitive description: name: coverage - url: "https://pub.dartlang.org" + sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.6.3" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted version: "3.0.2" custom_rounded_rectangle_border: dependency: transitive description: name: custom_rounded_rectangle_border - url: "https://pub.dartlang.org" + sha256: "57b7af53da4e8bf4afa5a8393c446e953a81c23dd309f4341cfc38d19ff6f062" + url: "https://pub.dev" source: hosted version: "0.2.0-nullsafety.0" dbus: dependency: transitive description: name: dbus - url: "https://pub.dartlang.org" + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + url: "https://pub.dev" source: hosted version: "0.7.8" decorated_icon: dependency: "direct main" description: name: decorated_icon - url: "https://pub.dartlang.org" + sha256: "4e2af1906cd211905ee729960daef162cd9f0bacd1d24de99e84e37e114a3648" + url: "https://pub.dev" source: hosted version: "1.2.1" device_info_plus: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" + sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95" + url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.1.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" source: hosted version: "7.0.0" dynamic_color: dependency: "direct main" description: name: dynamic_color - url: "https://pub.dartlang.org" + sha256: c4a508284b14ec4dda5adba2c28b2cdd34fbae1afead7e8c52cad87d51c5405b + url: "https://pub.dev" source: hosted - version: "1.5.4" + version: "1.6.2" equatable: dependency: "direct main" description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted version: "2.0.5" event_bus: dependency: "direct main" description: name: event_bus - url: "https://pub.dartlang.org" + sha256: "44baa799834f4c803921873e7446a2add0f3efa45e101a054b1f0ab9b95f8edc" + url: "https://pub.dev" source: hosted version: "2.0.0" expansion_tile_card: @@ -246,7 +280,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: e0380a550c4be0ef52a70f398c2211fc503094e8 + resolved-ref: "42da4c7ecf8914303bf13a959f6740c9eb15d557" url: "https://github.com/deckerst/expansion_tile_card.git" source: git version: "2.0.0" @@ -254,14 +288,16 @@ packages: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted version: "1.3.1" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" source: hosted version: "2.0.1" fijkplayer: @@ -277,63 +313,72 @@ packages: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.4" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.dartlang.org" + sha256: be13e431c0c950f0fc66bdb67b41b8059121d7e7d8bbbc21fb59164892d561f8 + url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.5.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + sha256: "5615b30c36f55b2777d0533771deda7e5730e769e5d3cb7fda79e9bed86cfa55" + url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + sha256: "4b3a41410f3313bb95fd560aa5eb761b6ad65c185de772c72231e8b4aeed6d18" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics - url: "https://pub.dartlang.org" + sha256: "6f1dc5321aa7d8bb84e8eec2ed517b0a7e7bc99a2708549c55c78359a323ecd0" + url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "3.0.12" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - url: "https://pub.dartlang.org" + sha256: "3728ad632a5209499cdfed6a1f7154982a524a076566f5a1d02af623bf46f947" + url: "https://pub.dev" source: hosted - version: "3.3.10" + version: "3.3.12" flex_color_picker: dependency: "direct main" description: name: flex_color_picker - url: "https://pub.dartlang.org" + sha256: "607c9fdb26be84d4a5a0931ab42a7eda725372e4f5ebaa2526ab6b22ead752f9" + url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.1.0" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - url: "https://pub.dartlang.org" + sha256: e61950ccadfb8d43ce5cdef382e8f689edc053ce6b837e277539410ecfb3b3e5 + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.2" fluster: dependency: "direct main" description: name: fluster - url: "https://pub.dartlang.org" + sha256: "3807f5d088b7798f0416b8578498046338af98bb4fb922a70e2810b8293963f6" + url: "https://pub.dev" source: hosted version: "1.2.0" flutter: @@ -345,14 +390,16 @@ packages: dependency: transitive description: name: flutter_cube - url: "https://pub.dartlang.org" + sha256: "71cf679a251166eb97f86751c56582b09abdbf859485fbf60524948813914c3b" + url: "https://pub.dev" source: hosted version: "0.1.1" flutter_displaymode: dependency: "direct main" description: name: flutter_displaymode - url: "https://pub.dartlang.org" + sha256: "136b0314fdc78fe995b0b75061fe9ff8210dffca84f8f8110f8f71029479db3b" + url: "https://pub.dev" source: hosted version: "0.5.0" flutter_driver: @@ -364,14 +411,16 @@ packages: dependency: "direct main" description: name: flutter_highlight - url: "https://pub.dartlang.org" + sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" + url: "https://pub.dev" source: hosted version: "0.7.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_localizations: @@ -383,28 +432,32 @@ packages: dependency: "direct main" description: name: flutter_map - url: "https://pub.dartlang.org" + sha256: "59dfd14267b691bea55760786b47d3172d47cdcc0d79ff930746a5ad123491b8" + url: "https://pub.dev" source: hosted version: "3.1.0" flutter_markdown: dependency: "direct main" description: name: flutter_markdown - url: "https://pub.dartlang.org" + sha256: "818cf6c28377ba2c91ed283c96fd712e9c175dd2d2488eb7fc93b6afb9ad2e08" + url: "https://pub.dev" source: hosted - version: "0.6.13" + version: "0.6.13+1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b" + url: "https://pub.dev" source: hosted version: "2.0.7" flutter_staggered_animations: dependency: "direct main" description: name: flutter_staggered_animations - url: "https://pub.dartlang.org" + sha256: "81d3c816c9bb0dca9e8a5d5454610e21ffb068aedb2bde49d2f8d04f75538351" + url: "https://pub.dev" source: hosted version: "1.1.1" flutter_test: @@ -421,9 +474,10 @@ packages: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "3.2.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -433,182 +487,248 @@ packages: dependency: "direct main" description: name: get_it - url: "https://pub.dartlang.org" + sha256: "290fde3a86072e4b37dbb03c07bec6126f0ecc28dad403c12ffe2e5a2d751ab7" + url: "https://pub.dev" source: hosted version: "7.2.0" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" google_api_availability: dependency: transitive description: name: google_api_availability - url: "https://pub.dartlang.org" + sha256: a1f58c5213abae549fb1e1451bc68eb499da7033039f77bc289fd6faca384295 + url: "https://pub.dev" source: hosted version: "3.0.1" google_maps_flutter: dependency: transitive description: name: google_maps_flutter - url: "https://pub.dartlang.org" + sha256: "0c6b72b4b1e0f6204973e2b40868a75fe6380725d498f215cd7e35ed920d1c57" + url: "https://pub.dev" source: hosted version: "2.2.3" google_maps_flutter_android: dependency: transitive description: name: google_maps_flutter_android - url: "https://pub.dartlang.org" + sha256: f238a04c378df6fbe7d84a3ea19362e9db19ea1717d06deffa9edaa17973e916 + url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" google_maps_flutter_ios: dependency: transitive description: name: google_maps_flutter_ios - url: "https://pub.dartlang.org" + sha256: "33bbca8d4148ed373251ea2ec2344fdc63009926b6d6be71a0854fd42409b1ba" + url: "https://pub.dev" source: hosted version: "2.1.13" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface - url: "https://pub.dartlang.org" + sha256: "0967430c25240836b794d42336bd4c61f0e78e9fd33d1365fa9316bb36b6b410" + url: "https://pub.dev" source: hosted version: "2.2.5" highlight: dependency: transitive description: name: highlight - url: "https://pub.dartlang.org" + sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" + url: "https://pub.dev" source: hosted version: "0.7.0" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted version: "4.0.2" image: dependency: transitive description: name: image - url: "https://pub.dartlang.org" + sha256: "3686865febd85c57a632d87a0fb1f0a8a9ef602fdb68d909c3e9aa29ec70fd24" + url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "4.0.13" intl: dependency: "direct main" description: name: intl - url: "https://pub.dartlang.org" + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" latlong2: dependency: "direct main" description: name: latlong2 - url: "https://pub.dartlang.org" + sha256: "408993a0e3f46e79ce1f129e4cb0386eef6d48dfa6394939ecacfbd7049154ec" + url: "https://pub.dev" source: hosted version: "0.8.1" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" lists: dependency: transitive description: name: lists - url: "https://pub.dartlang.org" + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" source: hosted version: "1.0.1" + local_auth: + dependency: "direct main" + description: + name: local_auth + sha256: "8cea55dca20d1e0efa5480df2d47ae30851e7a24cb8e7d225be7e67ae8485aa4" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + sha256: cfcbc4936e288d61ef85a04feef6b95f49ba496d4fd98364e6abafb462b06a1f + url: "https://pub.dev" + source: hosted + version: "1.0.18" + local_auth_ios: + dependency: transitive + description: + name: local_auth_ios + sha256: aa32478d7513066564139af57e11e2cad1bbd535c1efd224a88a8764c5665e3b + url: "https://pub.dev" + source: hosted + version: "1.0.12" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + sha256: fbb6973f2fd088e2677f39a5ab550aa1cfbc00997859d5e865569872499d6d61 + url: "https://pub.dev" + source: hosted + version: "1.0.6" + local_auth_windows: + dependency: transitive + description: + name: local_auth_windows + sha256: "888482e4f9ca3560e00bc227ce2badeb4857aad450c42a31c6cfc9dc21e0ccbc" + url: "https://pub.dev" + source: hosted + version: "1.0.5" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" markdown: dependency: transitive description: name: markdown - url: "https://pub.dartlang.org" + sha256: c2b81e184067b41d0264d514f7cdaa2c02d38511e39d6521a1ccc238f6d7b3f2 + url: "https://pub.dev" source: hosted version: "6.0.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.13" material_color_utilities: dependency: "direct main" description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" material_design_icons_flutter: dependency: "direct main" description: name: material_design_icons_flutter - url: "https://pub.dartlang.org" + sha256: "8ef8562d16e747b2d93e5da5c2508931588939c5c00ebc8e2768e803db7dfd3c" + url: "https://pub.dev" source: hosted version: "6.0.7096" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" mgrs_dart: dependency: transitive description: name: mgrs_dart - url: "https://pub.dartlang.org" + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" source: hosted version: "2.0.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" source: hosted version: "1.0.4" motion_sensors: @@ -624,56 +744,64 @@ packages: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" nm: dependency: transitive description: name: nm - url: "https://pub.dartlang.org" + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" source: hosted version: "0.5.0" node_preamble: dependency: transitive description: name: node_preamble - url: "https://pub.dartlang.org" + sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d" + url: "https://pub.dev" source: hosted version: "2.0.1" overlay_support: dependency: "direct main" description: name: overlay_support - url: "https://pub.dartlang.org" + sha256: fc39389bfd94e6985e1e13b2a88a125fc4027608485d2d4e2847afe1b2bb339c + url: "https://pub.dev" source: hosted version: "2.1.0" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" + sha256: "8df5ab0a481d7dc20c0e63809e90a588e496d276ba53358afc4c4443d0a00697" + url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" source: hosted version: "2.0.1" palette_generator: dependency: "direct main" description: name: palette_generator - url: "https://pub.dartlang.org" + sha256: "0e3cd6974e10b1434dcf4cf779efddb80e2696585e273a2dbede6af52f94568d" + url: "https://pub.dev" source: hosted version: "0.3.3+2" panorama: @@ -689,287 +817,344 @@ packages: dependency: "direct main" description: name: path - url: "https://pub.dartlang.org" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" source: hosted version: "1.8.2" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.dartlang.org" + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" source: hosted version: "1.0.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + url: "https://pub.dev" source: hosted version: "2.1.7" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + url: "https://pub.dev" source: hosted version: "2.0.5" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + url: "https://pub.dev" source: hosted version: "2.1.3" pdf: dependency: "direct main" description: name: pdf - url: "https://pub.dartlang.org" + sha256: "6cd57e8e6d052bd1078f18e0dc7cd6701fad6288231c1ce99d66ef5034d14e61" + url: "https://pub.dev" source: hosted - version: "3.8.4" + version: "3.9.0" percent_indicator: dependency: "direct main" description: name: percent_indicator - url: "https://pub.dartlang.org" + sha256: cec41f67181fbd5322aa68b355621d1a4eea827426b8eeb613f6cbe195ff7b4a + url: "https://pub.dev" source: hosted version: "4.2.2" permission_handler: dependency: "direct main" description: name: permission_handler - url: "https://pub.dartlang.org" + sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8" + url: "https://pub.dev" source: hosted version: "10.2.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - url: "https://pub.dartlang.org" + sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2" + url: "https://pub.dev" source: hosted version: "10.2.0" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - url: "https://pub.dartlang.org" + sha256: "9c370ef6a18b1c4b2f7f35944d644a56aa23576f23abee654cf73968de93f163" + url: "https://pub.dev" source: hosted version: "9.0.7" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - url: "https://pub.dartlang.org" + sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84" + url: "https://pub.dev" source: hosted version: "3.9.0" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - url: "https://pub.dartlang.org" + sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b + url: "https://pub.dev" source: hosted version: "0.1.2" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.dev" source: hosted version: "5.1.0" + pinput: + dependency: "direct main" + description: + name: pinput + sha256: e6aabd1571dde622f9b942f62ac2c80f84b0b50f95fa209a93e78f7d621e1f82 + url: "https://pub.dev" + source: hosted + version: "2.2.23" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + url: "https://pub.dev" source: hosted version: "2.1.3" polylabel: dependency: transitive description: name: polylabel - url: "https://pub.dartlang.org" + sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" + url: "https://pub.dev" source: hosted version: "1.0.1" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted version: "1.5.1" positioned_tap_detector_2: dependency: transitive description: name: positioned_tap_detector_2 - url: "https://pub.dartlang.org" + sha256: "52e06863ad3e1f82b058fd05054fc8c9caeeb3b47d5cea7a24bd9320746059c1" + url: "https://pub.dev" source: hosted version: "1.0.4" printing: dependency: "direct main" description: name: printing - url: "https://pub.dartlang.org" + sha256: fe654363cd0114b50a0815b24e96957c7e9a60eb4e3b7ccfe71cf3f2b7114cb2 + url: "https://pub.dev" source: hosted - version: "5.9.3" + version: "5.10.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" source: hosted version: "4.2.4" proj4dart: dependency: "direct main" description: name: proj4dart - url: "https://pub.dartlang.org" + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" source: hosted version: "2.1.0" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted version: "6.0.5" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" source: hosted version: "2.1.3" qr: dependency: transitive description: name: qr - url: "https://pub.dartlang.org" + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" source: hosted version: "3.0.1" screen_brightness: dependency: "direct main" description: name: screen_brightness - url: "https://pub.dartlang.org" + sha256: "62fd61a64e68b32b98b840bad7d8b6822bbc40e63c2b569a5f85528484c86b41" + url: "https://pub.dev" source: hosted version: "0.2.2" screen_brightness_android: dependency: transitive description: name: screen_brightness_android - url: "https://pub.dartlang.org" + sha256: "4e4ba0c44b5c24be20030733ada0c844aa0e8f1963f5d7cd72f5b2fe54a61495" + url: "https://pub.dev" source: hosted version: "0.1.0" screen_brightness_ios: dependency: transitive description: name: screen_brightness_ios - url: "https://pub.dartlang.org" + sha256: "99adc3ca5490b8294284aad5fcc87f061ad685050e03cf45d3d018fe398fd9a2" + url: "https://pub.dev" source: hosted version: "0.1.0" screen_brightness_macos: dependency: transitive description: name: screen_brightness_macos - url: "https://pub.dartlang.org" + sha256: "64b34e7e3f4900d7687c8e8fb514246845a73ecec05ab53483ed025bd4a899fd" + url: "https://pub.dev" source: hosted version: "0.1.0+1" screen_brightness_platform_interface: dependency: transitive description: name: screen_brightness_platform_interface - url: "https://pub.dartlang.org" + sha256: b211d07f0c96637a15fb06f6168617e18030d5d74ad03795dd8547a52717c171 + url: "https://pub.dev" source: hosted version: "0.1.0" screen_brightness_windows: dependency: transitive description: name: screen_brightness_windows - url: "https://pub.dartlang.org" + sha256: "80d90ecdc63fc0823f2ecb1be323471619287937e14210650d7b25ca181abd05" + url: "https://pub.dev" source: hosted version: "0.1.1" + screen_state: + dependency: "direct main" + description: + name: screen_state + sha256: "39184c718baf303f26200f6b1392b12a549d88410e907e046d75594588c0df5d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9" + url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.0.17" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7" + url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.0.15" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - url: "https://pub.dartlang.org" + sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.3" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874 + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" shared_preferences_platform_interface: - dependency: transitive + dependency: "direct dev" description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3 + url: "https://pub.dev" source: hosted version: "2.1.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958 + url: "https://pub.dev" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" source: hosted version: "1.4.0" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler - url: "https://pub.dartlang.org" + sha256: aef74dc9195746a384843102142ab65b6a4735bb3beea791e63527b88cc83306 + url: "https://pub.dev" source: hosted version: "3.0.1" shelf_static: dependency: transitive description: name: shelf_static - url: "https://pub.dartlang.org" + sha256: e792b76b96a36d4a41b819da593aff4bdd413576b3ba6150df5d8d9996d2e74c + url: "https://pub.dev" source: hosted version: "1.1.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" source: hosted version: "1.0.3" sky_engine: @@ -977,67 +1162,84 @@ packages: description: flutter source: sdk version: "0.0.99" + smart_auth: + dependency: transitive + description: + name: smart_auth + sha256: "8cfaec55b77d5930ed1666bb7ae70db5bade099bb1422401386853b400962113" + url: "https://pub.dev" + source: hosted + version: "1.0.8" smooth_page_indicator: dependency: "direct main" description: name: smooth_page_indicator - url: "https://pub.dartlang.org" + sha256: "49e9b6a265790454c39bd4a447a02f398c02b44b2602e7c5e3a381dc2e3b4102" + url: "https://pub.dev" source: hosted version: "1.0.0+2" source_map_stack_trace: dependency: transitive description: name: source_map_stack_trace - url: "https://pub.dartlang.org" + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" source: hosted version: "2.1.1" source_maps: dependency: transitive description: name: source_maps - url: "https://pub.dartlang.org" + sha256: "490098075234dcedb83c5d949b4c93dad5e6b7702748de000be2b57b8e6b2427" + url: "https://pub.dev" source: hosted version: "0.10.11" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" sqflite: dependency: "direct main" description: name: sqflite - url: "https://pub.dartlang.org" + sha256: "78324387dc81df14f78df06019175a86a2ee0437624166c382e145d0a7fd9a4f" + url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.4+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f + url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2+2" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted version: "2.1.0" streams_channel: @@ -1053,219 +1255,258 @@ packages: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" sync_http: dependency: transitive description: name: sync_http - url: "https://pub.dartlang.org" + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" source: hosted version: "0.3.1" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" + url: "https://pub.dev" source: hosted version: "3.0.1" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" test: dependency: "direct dev" description: name: test - url: "https://pub.dartlang.org" + sha256: a5fcd2d25eeadbb6589e80198a47d6a464ba3e2049da473943b8af9797900c2d + url: "https://pub.dev" source: hosted - version: "1.21.4" + version: "1.22.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.4.16" test_core: dependency: transitive description: name: test_core - url: "https://pub.dartlang.org" + sha256: "0ef9755ec6d746951ba0aabe62f874b707690b5ede0fecc818b138fcc9b14888" + url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.4.20" transparent_image: dependency: "direct main" description: name: transparent_image - url: "https://pub.dartlang.org" + sha256: e566a616922a781489f4d91cc939b1b3203b6e4a093317805f2f82f0bb0f8dec + url: "https://pub.dev" source: hosted version: "2.0.0" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" + url: "https://pub.dev" source: hosted version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" unicode: dependency: transitive description: name: unicode - url: "https://pub.dartlang.org" + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" source: hosted version: "0.3.1" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b + url: "https://pub.dev" source: hosted - version: "6.1.7" + version: "6.1.9" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" + url: "https://pub.dev" source: hosted - version: "6.0.22" + version: "6.0.23" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815" + url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.1.0" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6" + url: "https://pub.dev" source: hosted version: "2.1.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" + url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.14" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" vm_service: dependency: transitive description: name: vm_service - url: "https://pub.dartlang.org" + sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 + url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "9.4.0" volume_controller: dependency: "direct main" description: name: volume_controller - url: "https://pub.dartlang.org" + sha256: "3bcaadd0e3298286c4ef3b758a3ea44fc98a9abe24d0e7ef087bfab72a4b6924" + url: "https://pub.dev" source: hosted version: "2.0.6" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" source: hosted version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" source: hosted version: "2.3.0" webdriver: dependency: transitive description: name: webdriver - url: "https://pub.dartlang.org" + sha256: ef67178f0cc7e32c1494645b11639dd1335f1d18814aa8435113a92e9ef9d841 + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol - url: "https://pub.dartlang.org" + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + url: "https://pub.dev" source: hosted version: "1.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + url: "https://pub.dev" source: hosted version: "3.1.3" wkt_parser: dependency: transitive description: name: wkt_parser - url: "https://pub.dartlang.org" + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" source: hosted version: "2.0.0" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + url: "https://pub.dev" source: hosted version: "0.2.0+3" xml: dependency: "direct main" description: name: xml - url: "https://pub.dartlang.org" + sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.2" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted version: "3.1.1" sdks: - dart: ">=2.18.0 <3.0.0" - flutter: ">=3.3.10" + dart: ">=2.19.0 <3.0.0" + flutter: ">=3.7.3" diff --git a/pubspec.yaml b/pubspec.yaml index 1b166e0ca..270faa71b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,14 +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.7.10+90 +version: 1.8.0+91 publish_to: none environment: # this project bundles Flutter SDK via `flutter_wrapper` # cf https://github.com/passsy/flutter_wrapper - flutter: 3.3.10 - sdk: ">=2.18.0 <3.0.0" + flutter: 3.7.3 + sdk: ">=2.19.0 <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://` @@ -69,6 +69,7 @@ dependencies: get_it: intl: latlong2: + local_auth: material_color_utilities: material_design_icons_flutter: overlay_support: @@ -82,10 +83,12 @@ dependencies: pdf: percent_indicator: permission_handler: + pinput: printing: proj4dart: provider: screen_brightness: + screen_state: shared_preferences: smooth_page_indicator: sqflite: @@ -104,6 +107,7 @@ dev_dependencies: flutter_driver: sdk: flutter flutter_lints: + shared_preferences_platform_interface: test: flutter: diff --git a/shaders.sksl.json b/shaders.sksl.json index b1218407f..64f321fb2 100644 --- a/shaders.sksl.json +++ b/shaders.sksl.json @@ -1 +1 @@ -{"platform":"android","name":"SM G970N","engineRevision":"3316dd8728419ad3534e3f6112aa6291f587078a","data":{"AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HUJBYAQCAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAACeBwAAdW5pZm9ybSBoYWxmIHVTcmNURl9TMFs3XTsKdW5pZm9ybSBoYWxmM3gzIHVDb2xvclhmb3JtX1MwOwp1bmlmb3JtIGhhbGYgdURzdFRGX1MwWzddOwp1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1MxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZiBzcmNfdGZfUzAoaGFsZiB4KSAKewoJaGFsZiBHID0gdVNyY1RGX1MwWzBdOwoJaGFsZiBBID0gdVNyY1RGX1MwWzFdOwoJaGFsZiBCID0gdVNyY1RGX1MwWzJdOwoJaGFsZiBDID0gdVNyY1RGX1MwWzNdOwoJaGFsZiBEID0gdVNyY1RGX1MwWzRdOwoJaGFsZiBFID0gdVNyY1RGX1MwWzVdOwoJaGFsZiBGID0gdVNyY1RGX1MwWzZdOwoJaGFsZiBzID0gc2lnbih4KTsKCXggPSBhYnMoeCk7Cgl4ID0gKHggPCBEKSA/IChDICogeCkgKyBGIDogcG93KEEgKiB4ICsgQiwgRykgKyBFOwoJcmV0dXJuIHMgKiB4Owp9CmhhbGYgZHN0X3RmX1MwKGhhbGYgeCkgCnsKCWhhbGYgRyA9IHVEc3RURl9TMFswXTsKCWhhbGYgQSA9IHVEc3RURl9TMFsxXTsKCWhhbGYgQiA9IHVEc3RURl9TMFsyXTsKCWhhbGYgQyA9IHVEc3RURl9TMFszXTsKCWhhbGYgRCA9IHVEc3RURl9TMFs0XTsKCWhhbGYgRSA9IHVEc3RURl9TMFs1XTsKCWhhbGYgRiA9IHVEc3RURl9TMFs2XTsKCWhhbGYgcyA9IHNpZ24oeCk7Cgl4ID0gYWJzKHgpOwoJeCA9ICh4IDwgRCkgPyAoQyAqIHgpICsgRiA6IHBvdyhBICogeCArIEIsIEcpICsgRTsKCXJldHVybiBzICogeDsKfQpoYWxmNCBnYW11dF94Zm9ybV9TMChoYWxmNCBjb2xvcikgCnsKCWNvbG9yLnJnYiA9ICh1Q29sb3JYZm9ybV9TMCAqIGNvbG9yLnJnYik7CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgY29sb3JfeGZvcm1fUzAoZmxvYXQ0IGNvbG9yKSAKewoJY29sb3IuciA9IHNyY190Zl9TMChoYWxmKGNvbG9yLnIpKTsKCWNvbG9yLmcgPSBzcmNfdGZfUzAoaGFsZihjb2xvci5nKSk7Cgljb2xvci5iID0gc3JjX3RmX1MwKGhhbGYoY29sb3IuYikpOwoJY29sb3IgPSBnYW11dF94Zm9ybV9TMChoYWxmNChjb2xvcikpOwoJY29sb3IuciA9IGRzdF90Zl9TMChoYWxmKGNvbG9yLnIpKTsKCWNvbG9yLmcgPSBkc3RfdGZfUzAoaGFsZihjb2xvci5nKSk7Cgljb2xvci5iID0gZHN0X3RmX1MwKGhhbGYoY29sb3IuYikpOwoJcmV0dXJuIGhhbGY0KGNvbG9yKTsKfQpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKGNvbG9yX3hmb3JtX1MwKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpKSAqIGhhbGY0KDEpKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAEAQAAAAGQCBAMQACAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAGUDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMC54eSwgdWNsYW1wX1MxX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","DBAAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAIAAAAAAAAAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1NVAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludDIgY29vcmRzID0gaW50MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJaW50IHRleElkeCA9IGNvb3Jkcy54ID4+IDEzOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGNvb3Jkcy54ICYgMHgxRkZGLCBjb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAAFoCAAB1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzFfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7CglpZiAodlRleEluZGV4X1MwID09IDApIAoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWVsc2UgCgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzFfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAIAAEAAAABJYQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAEcGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TMV9jMF9jMF9jMC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueik7CgljbGFtcGVkQ29vcmQueSA9IHN1YnNldENvb3JkLnk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAAA5AAAAAAABAAAAACAZAAAAA":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgAAAABdAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2YXJjY29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAAAQAAAAGQCBAMQACAAAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAIgDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzAueCwgdWNsYW1wX1MxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","B2AAQAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CQAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cgl2aW5Db3ZlcmFnZV9TMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAZQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBoYWxmIHZpbkNvdmVyYWdlX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdUNvbG9yX1MwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACRAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAFBQATAAAAAAFAAMAAAABAAAAAAABBAMAAAAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAABYBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgRWxsaXB0aWNhbFJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJZmxvYXQyIFogPSBkeHkgKiB1aW52UmFkaWlYWV9TMS54eTsKCWhhbGYgaW1wbGljaXQgPSBoYWxmKGRvdChaLCBkeHkpIC0gMS4wKTsKCWhhbGYgZ3JhZF9kb3QgPSBoYWxmKDQuMCAqIGRvdChaLCBaKSk7CglncmFkX2RvdCA9IG1heChncmFkX2RvdCwgMS4wZS00KTsKCWhhbGYgYXBwcm94X2Rpc3QgPSBpbXBsaWNpdCAqIGhhbGYoaW52ZXJzZXNxcnQoZ3JhZF9kb3QpKTsKCWhhbGYgYWxwaGEgPSBjbGFtcCgwLjUgKyBhcHByb3hfZGlzdCwgMC4wLCAxLjApOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRWxsaXB0aWNhbFJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAB3QA6AAAEAAAAAAAMAAPEAEAAABAAAAAAB2AAAAAAACAAAAAEBSAAAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAA2BQAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzI7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MyOwppbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglhbHBoYSA9IDEuMCAtIGFscGhhOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CmhhbGY0IENpcmN1bGFyUlJlY3RfUzIoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MyLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MyLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzIueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCWhhbGY0IG91dHB1dF9TMjsKCW91dHB1dF9TMiA9IENpcmN1bGFyUlJlY3RfUzIob3V0cHV0X1MxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMjsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","HVJAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAABAAAAAABBAMABAAOAAAABAAAAAAABBAMAAA":"CQAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAADoAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfUzApKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAABAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAIAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIA":"CQAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAOMGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgwKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IENsYW1wZWRHcmFkaWVudF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF81X2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIBAIAAAABLCIIBAAAAABAEGABBAMAACAIAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADhAwAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfUzFfYzBfYzAueHksIHVjbGFtcF9TMV9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoTWF0cml4RWZmZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvbG9yX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CQAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAgBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdCB2Y292ZXJhZ2VfUzA7CmZsYXQgaW4gZmxvYXQ0IHZnZW9tU3Vic2V0X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAQAAAAAAAAA=","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAyQEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEADZABYAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAADsAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAEAAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIBSQB5VRECGAEAAAMAAAAAAAAAAACAA4AAAACAAAAAAACCAYAA":"CQAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAA4wQAAGNvbnN0IGludCBrRmlsbEJXX1MxID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGNvdmVyYWdlOwoJaWYgKGludCgxKSA9PSBrRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fUzEuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1MxLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7Cgl9CgllbHNlIAoJewoJCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1MxKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCWlmIChpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIGNvdmVyYWdlKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAAcBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQAAEAQAAAAGQCBAMQAAAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAIgDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzAueSwgdWNsYW1wX1MxX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAACEA2X4PLOGEAAAAAAAAACAAAAAVQQAAQAAAAAQCDIBCAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAAAAAAA":"CQAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAACwQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBDaXJjbGVCbHVyX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TMS54eSkpICogZmxvYXQodWNpcmNsZURhdGFfUzEudykpOwoJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfUzEueikgKiB1Y2lyY2xlRGF0YV9TMS53OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIE1hdHJpeEVmZmVjdF9TMV9jMChfdG1wXzBfaW5Db2xvciwgZmxvYXQyKGhhbGYyKGRpc3QsIDAuNSkpKS53KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZUJsdXJfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABGAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAAAAIAAAABLAIABAAAAABAEGABBAMAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADOAgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TMV9jMChfc3JjKSwgX3NyYyk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb2xvcl9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAZCBRE4GNEACAAAOAAAAAAAAAAABAAOAAAABAAAAAAABBAMAA":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAAAHBQAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY2xlX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZiBkOwoJaWYgKGludCgzKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWQgPSBoYWxmKChsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSAtIDEuMCkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJZWxzZSAKCXsKCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1MxLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfUzEudykpICogdWNpcmNsZV9TMS56KTsKCX0KCWlmIChpbnQoMykgPT0ga0ZpbGxBQV9TMSB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCXJldHVybiBoYWxmNChfaW5wdXQgKiBzYXR1cmF0ZShkKSk7Cgl9CgllbHNlIAoJewoJCXJldHVybiBoYWxmNChkID4gMC41ID8gX2lucHV0IDogaGFsZjQoMC4wKSk7Cgl9Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZV9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","EABQAAAAAEAAAAAQAABQAAIOAAABCFYIAAKAUDAAAAAAAAABAAAAAAAAAAANAAIAAAABAAAAACAJAAIAAAAA":"CQAAAExTS1OhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgZmxvYXQyIHZJbnRUZXh0dXJlQ29vcmRzX1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJdkludFRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkczsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8yX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEaXN0YW5jZUZpZWxkUGF0aAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfUzA7CgloYWxmNCB0ZXhDb2xvcjsKCXsKCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLnJycnI7Cgl9CgloYWxmIGRpc3RhbmNlID0gNy45Njg3NSoodGV4Q29sb3IuciAtIDAuNTAxOTYwNzg0MzEpOwoJaGFsZiBhZndpZHRoOwoJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeCh2SW50VGV4dHVyZUNvb3Jkc19TMC54KSkpOwoJaGFsZiB2YWwgPSBzbW9vdGhzdGVwKC1hZndpZHRoLCBhZndpZHRoLCBkaXN0YW5jZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KHZhbCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAKPABAAAAAAB2AAAAAAACAAAAAEBSAAAAAAA":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAACzBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBFbGxpcHRpY2FsUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CglmbG9hdDIgWiA9IGR4eSAqIHVpbnZSYWRpaVhZX1MxLnh5OwoJaGFsZiBpbXBsaWNpdCA9IGhhbGYoZG90KFosIGR4eSkgLSAxLjApOwoJaGFsZiBncmFkX2RvdCA9IGhhbGYoNC4wICogZG90KFosIFopKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjBlLTQpOwoJaGFsZiBhcHByb3hfZGlzdCA9IGltcGxpY2l0ICogaGFsZihpbnZlcnNlc3FydChncmFkX2RvdCkpOwoJaGFsZiBhbHBoYSA9IGNsYW1wKDAuNSArIGFwcHJveF9kaXN0LCAwLjAsIDEuMCk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEVsbGlwdGljYWxSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","BYIBQAAABQAAIAABBYAAAEIXBAAP777777777777AAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CQAAAExTS1M+AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzNfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAHgEAAGluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAEAAAAAAAAA","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAAEAQCAAAAAVREEAQAAAAAQCDAAQQGAABAEAAAAAAH4AQAAAAAEAAAAAQGIAAAAAAA":"CQAAAExTS1PfAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMCkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAEAFAAAjZXh0ZW5zaW9uIEdMX0VYVF9zaGFkZXJfZnJhbWVidWZmZXJfZmV0Y2g6IHJlcXVpcmUKdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1MwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cmlub3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMF9jMC54eSwgdWNsYW1wX1MxX2MwX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShoYWxmNCBfaW5wdXQpIAp7CglfaW5wdXQgPSBNYXRyaXhFZmZlY3RfUzFfYzAoX2lucHV0KTsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IGNvdmVyYWdlID0gdmNvdmVyYWdlX1MwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYgU2hhZGVyCgkJaGFsZjQgX2RzdENvbG9yID0gc2tfRnJhZ0NvbG9yOwoJCXNrX0ZyYWdDb2xvciA9IGJsZW5kX3NyYyhvdXRwdXRfUzEsIF9kc3RDb2xvcik7CgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q292ZXJhZ2VfUzAgKiBza19GcmFnQ29sb3IgKyAoaGFsZjQoMS4wKSAtIG91dHB1dENvdmVyYWdlX1MwKSAqIF9kc3RDb2xvcjsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAQAAAAAAAAAAAAAABAAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","DASAAAAAQAAWAABAYAAQBYH7777Z6QQBAEAAAAAAEAAAAAAAEBSAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1PVAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc1NpemVJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAD4AQAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfUzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHVDb2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCk7Cgl9CglvdXRwdXRDb2xvcl9TMCA9IG91dHB1dENvbG9yX1MwICogdGV4Q29sb3I7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CQAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAAuAIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAAEAACAAAAAVREAAQAAAAAQCDAAQQGAABAAAAAAAAH4AQAAAAAEAAAAAQGIAAAAAAA":"CQAAAExTS1PfAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMCkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAGMFAAAjZXh0ZW5zaW9uIEdMX0VYVF9zaGFkZXJfZnJhbWVidWZmZXJfZmV0Y2g6IHJlcXVpcmUKdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1MwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cmlub3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzBfYzAueCwgdWNsYW1wX1MxX2MwX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IE1hdHJpeEVmZmVjdF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgY292ZXJhZ2UgPSB2Y292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGhhbGYoY292ZXJhZ2UpKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZiBTaGFkZXIKCQloYWxmNCBfZHN0Q29sb3IgPSBza19GcmFnQ29sb3I7CgkJc2tfRnJhZ0NvbG9yID0gYmxlbmRfc3JjKG91dHB1dF9TMSwgX2RzdENvbG9yKTsKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb3ZlcmFnZV9TMCAqIHNrX0ZyYWdDb2xvciArIChoYWxmNCgxLjApIC0gb3V0cHV0Q292ZXJhZ2VfUzApICogX2RzdENvbG9yOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAQAAAAAAAAAAAAAABAAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HTQAAGAABBYAAAEIXBAAAGEAMAAAAAAAAAAAAAAAQAHAAAAAQAAAAAAAQQGAAAAA":"CQAAAExTS1M/AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgABAAAAHQMAAGluIGZsb2F0NCB2UXVhZEVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGYgZWRnZUFscGhhOwoJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1MwLnosIHZRdWFkRWRnZV9TMC53KSArIDAuNSwgMS4wKSk7Cgl9CgllbHNlIAoJewoJCWhhbGYyIGdGID0gaGFsZjIoaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TMC54KnZRdWFkRWRnZV9TMC54IC0gdlF1YWRFZGdlX1MwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAADAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAYAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAIAAQAAAAAQGIA":"CQAAAExTS1PlAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc181X1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMSkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAAAAMAcAAHVuaWZvcm0gaGFsZjQgdXN0YXJ0X1MxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVlbmRfUzFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1MwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzFfY29vcmRzID0gX2Nvb3JkczsKCXJldHVybiBoYWxmNChtaXgodXN0YXJ0X1MxX2MwX2MwLCB1ZW5kX1MxX2MwX2MwLCBoYWxmKF90bXBfMV9jb29yZHMueCkpKTsKfQpoYWxmNCBMaW5lYXJMYXlvdXRfUzFfYzBfYzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8yX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8zX2Nvb3JkcyA9IHZUcmFuc2Zvcm1lZENvb3Jkc181X1MwOwoJcmV0dXJuIGhhbGY0KGhhbGY0KGhhbGYoX3RtcF8zX2Nvb3Jkcy54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDYsIDEuMCwgMC4wLCAwLjApKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckxheW91dF9TMV9jMF9jMV9jMChfaW5wdXQpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzRfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoX3RtcF80X2luQ29sb3IpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIWJvb2woaW50KDEpKSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKF90bXBfNF9pbkNvbG9yLCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglpZiAoYm9vbChpbnQoMSkpKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShoYWxmNCBfaW5wdXQpIAp7CglfaW5wdXQgPSBDbGFtcGVkR3JhZGllbnRfUzFfYzAoX2lucHV0KTsKCWhhbGY0IF90bXBfNV9pbkNvbG9yID0gX2lucHV0OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IGNvdmVyYWdlID0gdmNvdmVyYWdlX1MwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSAoaGFsZjQoMS4wKSAtIG91dHB1dF9TMSkgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAGIBIAAABAAAAANAEAAAAAAAAAAAAAABAAOAAAABAAAAAAABBAMAAAAA":"CQAAAExTS1N0AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMl9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAIsCAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwKS5ycnJyOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAADAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAYAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIA":"CQAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAOMGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgxKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IENsYW1wZWRHcmFkaWVudF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF81X2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CQAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAkAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACtBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CQAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAACzAgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGACQAGAAAAAQAAAAAAAQQGAAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGAARAGQWMHGBRIAAAAABQAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABhBAAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjbGVfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGQ7CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxLncpIC0gMS4wKSAqIHVjaXJjbGVfUzEueik7Cgl9CgllbHNlIAoJewoJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJaWYgKGludCgxKSA9PSBrRmlsbEFBX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIHNhdHVyYXRlKGQpKTsKCX0KCWVsc2UgCgl7CgkJcmV0dXJuIGhhbGY0KGQgPiAwLjUgPyBfaW5wdXQgOiBoYWxmNCgwLjApKTsKCX0KfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJZmxvYXQyIHRleENvb3JkOwoJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TMDsKCW91dHB1dENvbG9yX1MwID0gKChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHRleENvb3JkKSAqIGhhbGY0KDEpKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY2xlX1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","AYAA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1OCAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMl9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","GEMAAAYAAEHAAAARC4EAAAQWBQAAAAAAAAAQAAAAIBCAAAGQAEAAAAAQAAAABAEQAEAAAAA":"CQAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAjAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAOAAAAaW5TaGFkb3dQYXJhbXMAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAAAAEAAAABJYQAAAAAAAAIAAAAAWCBAAAABAAAAANAECAZAAAAAAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAAgFAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzA7CnVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TMV9jMDsKdW5pZm9ybSBoYWxmMiB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFsxM107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgX2Nvb3Jkcyk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChfaW5wdXQsIGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzApICogX2Nvb3Jkcy54eTEpOwp9CmhhbGY0IFNtb290aF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBjb29yZCwgaGFsZjIgb2Zmc2V0QW5kS2VybmVsKSAKewoJcmV0dXJuIE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChjb29yZCArIG9mZnNldEFuZEtlcm5lbC54ICogdUluY3JlbWVudF9TMV9jMCkpICogb2Zmc2V0QW5kS2VybmVsLnk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBjb2xvciA9IGhhbGY0KDApOwoJZmxvYXQyIGNvb3JkID0gdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cglmb3IgKGludCBpPTA7IGk8MTM7ICsraSkgCgl7CgkJY29sb3IgKz0gU21vb3RoX1MxX2MwKF9pbnB1dCwgY29vcmQsIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwW2ldKTsKCX0KCXJldHVybiBjb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAACTAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CQAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAAKQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAEQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABPAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAHSADQAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAAWAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABZQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACnAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CQAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAACRAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKZmxhdCBpbiBmbG9hdDQgdmdlb21TdWJzZXRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAABAEAAAABJYQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAEcGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TMV9jMF9jMF9jMC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","B2ABSAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CQAAAExTS1N4AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZiBpbkNvdmVyYWdlOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gdUNvbG9yX1MwOwoJY29sb3IgPSBjb2xvciAqIGluQ292ZXJhZ2U7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8zX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzFfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAeAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA"}} \ No newline at end of file +{"platform":"android","name":"SM G970N","engineRevision":"248290d6d50a1bf1a6a246544347c03acda867a5","data":{"AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABZQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACPAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","B2ABSAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1N4AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZiBpbkNvdmVyYWdlOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gdUNvbG9yX1MwOwoJY29sb3IgPSBjb2xvciAqIGluQ292ZXJhZ2U7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8zX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzFfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAGAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGACQAGAAAAAQAAAAAAAQQGAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAACfAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEADZABYAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAADUAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAADEAANAAAAALHCKLMRAAAAAAAAABAAAAAGJBCFLQVBWAQAAAAAAQAAAAAMACQCAACAAAAA2AIBAEIAAAAAAAAAAAAIADQAAAAIAAAAAAAIIDAAAAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAAeAQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBDaXJjbGVCbHVyX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TMV9jMC54eSkpICogZmxvYXQodWNpcmNsZURhdGFfUzFfYzAudykpOwoJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfUzFfYzAueikgKiB1Y2lyY2xlRGF0YV9TMV9jMC53OwoJcmV0dXJuIGhhbGY0KE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfdG1wXzBfaW5Db2xvciwgZmxvYXQyKGhhbGYyKGRpc3QsIDAuNSkpKS53d3d3KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKENpcmNsZUJsdXJfUzFfYzAoX3NyYyksIF9zcmMpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAAAAIAAAABLAIABAAAAABAEGABBAMAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAC2AgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TMV9jMChfc3JjKSwgX3NyYyk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb2xvcl9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAB3QA6AAAEAAAAAAAMAAPEAEAAABAAAAAAB2AAAAAAACAAAAAEBSAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAAeBQAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzI7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MyOwppbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglhbHBoYSA9IDEuMCAtIGFscGhhOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CmhhbGY0IENpcmN1bGFyUlJlY3RfUzIoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MyLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MyLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzIueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCWhhbGY0IG91dHB1dF9TMjsKCW91dHB1dF9TMiA9IENpcmN1bGFyUlJlY3RfUzIob3V0cHV0X1MxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMjsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAMAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQAAEAQAAAAGQCBAMQAAAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAHADAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzAueSwgdWNsYW1wX1MxX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAB5AgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKZmxhdCBpbiBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAAAAAA==","DASAAAAAQAAWAABAYAAQBYH7777Z6QQBAEAAAAAAEAAAAAAAEBSAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1PVAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc1NpemVJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAADgAQAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfUzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHVDb2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCk7Cgl9CglvdXRwdXRDb2xvcl9TMCA9IG91dHB1dENvbG9yX1MwICogdGV4Q29sb3I7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","GEMAAAYAAEHAAAARC4EAAAQWBQAAAAAAAAAQAAAAIBCAAAGQAEAAAAAQAAAABAEQAEAAAAA":"CgAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAALAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAOAAAAaW5TaGFkb3dQYXJhbXMAAAAAAAA=","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFQBQU7BTXIAAAAAAAAAACAAAAAVQEAAQAAAAAQCDAEQQGAAAAAAAAAAAA4IAPAAACAAAAAAAEABYAAAAEAAAAAAAEEBQA":"CgAAAExTS1N6AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfM19TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzApICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAABAAAArAQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMjsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzI7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IE1hdHJpeEVmZmVjdF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQpoYWxmNCBDaXJjdWxhclJSZWN0X1MyKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMi5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMi5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MyLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7CgloYWxmNCBvdXRwdXRfUzI7CglvdXRwdXRfUzIgPSBDaXJjdWxhclJSZWN0X1MyKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRfUzI7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAAAAAA==","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAGIBIAAABAAAAANAEAAAAAAAAAAAAAABAAOAAAABAAAAAAABBAMAAAAA":"CgAAAExTS1N0AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMl9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAHMCAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwKS5ycnJyOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAAYQADAAAEAFEURUKQKAAAYAAAAAAAAIAAAABSCICWKY2FAEAAAMAAAAAAAAAAAAAIADQAAAAIAAAAAAAIIDAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAB+BQAAY29uc3QgaW50IGtGaWxsQldfUzFfYzAgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzFfYzA7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgUmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzFfYzAgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxX2MwKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMV9jMC56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSk7Cgl9CgllbHNlIAoJewoJCWhhbGY0IGRpc3RzNCA9IHNhdHVyYXRlKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1MxX2MwKSk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzApIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoaGFsZjQoY292ZXJhZ2UpKTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKFJlY3RfUzFfYzAoX3NyYyksIF9zcmMpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA","AYAA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1OCAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMl9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAADqAQAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBQU7BTXIAAAAAACAAAAAAQFV5W6JEAAAAAYAAAABQEZ2AKAWAQAABAL6SYKDYAAAACAAAAAAQEGIAAAAACAWTWL3EYAAAADAAAAACADHIJJCYCAAAEAP2LRIPAAAAAIAAAAAAABTALI3F5SOAIAABQAAAAAABTUEUZMBAAAAAH5FYUXQAAAAAAAEAAAAAZMRGOQCQFQEAAAAAAAAAGARL2LXJHAAEAAAAAEAAAABSCQX5FQUHQAAAAAAAAAACAA4AAAABAACAAAACCAYAAAAA":"CgAAAExTS1PrAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzZfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc182X1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMF9jMSkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAMMHAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMF9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc182X1MwOwpoYWxmNCBTaW5nbGVJbnRlcnZhbENvbG9yaXplcl9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8xX2Nvb3JkcyA9IF9jb29yZHM7CglyZXR1cm4gaGFsZjQobWl4KHVzdGFydF9TMV9jMF9jMF9jMCwgdWVuZF9TMV9jMF9jMF9jMCwgaGFsZihfdG1wXzFfY29vcmRzLngpKSk7Cn0KaGFsZjQgTGluZWFyTGF5b3V0X1MxX2MwX2MwX2MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMl9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfM19jb29yZHMgPSB2VHJhbnNmb3JtZWRDb29yZHNfNl9TMDsKCXJldHVybiBoYWxmNChoYWxmNChoYWxmKF90bXBfM19jb29yZHMueCkgKyAxZS0wNSwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzBfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzBfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgY29sb3JfeGZvcm1fUzFfYzAoZmxvYXQ0IGNvbG9yKSAKewoJY29sb3IucmdiICo9IGNvbG9yLmE7CglyZXR1cm4gaGFsZjQoY29sb3IpOwp9CmhhbGY0IENvbG9yU3BhY2VYZm9ybV9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gY29sb3JfeGZvcm1fUzFfYzAoQ2xhbXBlZEdyYWRpZW50X1MxX2MwX2MwKF9pbnB1dCkpOwp9CmhhbGY0IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEoaGFsZjQgX2lucHV0KSAKewoJX2lucHV0ID0gQ29sb3JTcGFjZVhmb3JtX1MxX2MwKF9pbnB1dCk7CgloYWxmNCBfdG1wXzVfaW5Db2xvciA9IF9pbnB1dDsKCXJldHVybiBoYWxmNChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gKGhhbGY0KDEuMCkgLSBvdXRwdXRfUzEpICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAAAAEAAAABJYQAAAAAAAAIAAAAAWCBAAAABAAAAANAECAZAAAAAAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAPAEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzA7CnVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TMV9jMDsKdW5pZm9ybSBoYWxmMiB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFsxM107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgX2Nvb3Jkcyk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChfaW5wdXQsIGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzApICogX2Nvb3Jkcy54eTEpOwp9CmhhbGY0IFNtb290aF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBjb29yZCwgaGFsZjIgb2Zmc2V0QW5kS2VybmVsKSAKewoJcmV0dXJuIE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChjb29yZCArIG9mZnNldEFuZEtlcm5lbC54ICogdUluY3JlbWVudF9TMV9jMCkpICogb2Zmc2V0QW5kS2VybmVsLnk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBjb2xvciA9IGhhbGY0KDApOwoJZmxvYXQyIGNvb3JkID0gdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cglmb3IgKGludCBpPTA7IGk8MTM7ICsraSkgCgl7CgkJY29sb3IgKz0gU21vb3RoX1MxX2MwKF9pbnB1dCwgY29vcmQsIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwW2ldKTsKCX0KCXJldHVybiBjb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","CMRQCIAABBYAAAEIXBAAACDQMAABRAFAAAAAAAAAAAAAAAEABYAAAAEAAAAAAAEEBQAAAAA":"CgAAAExTS1MyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0MiBpbkVsbGlwc2VPZmZzZXQ7CmluIGZsb2F0NCBpbkVsbGlwc2VSYWRpaTsKb3V0IGZsb2F0MiB2RWxsaXBzZU9mZnNldHNfUzA7Cm91dCBmbG9hdDQgdkVsbGlwc2VSYWRpaV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBFbGxpcHNlR2VvbWV0cnlQcm9jZXNzb3IKCXZFbGxpcHNlT2Zmc2V0c19TMCA9IGluRWxsaXBzZU9mZnNldDsKCXZFbGxpcHNlUmFkaWlfUzAgPSBpbkVsbGlwc2VSYWRpaTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAHIDAABpbiBmbG9hdDIgdkVsbGlwc2VPZmZzZXRzX1MwOwppbiBmbG9hdDQgdkVsbGlwc2VSYWRpaV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBFbGxpcHNlR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0MiBvZmZzZXQgPSB2RWxsaXBzZU9mZnNldHNfUzAueHk7CglvZmZzZXQgKj0gdkVsbGlwc2VSYWRpaV9TMC54eTsKCWZsb2F0IHRlc3QgPSBkb3Qob2Zmc2V0LCBvZmZzZXQpIC0gMS4wOwoJZmxvYXQyIGdyYWQgPSAyLjAqb2Zmc2V0KnZFbGxpcHNlUmFkaWlfUzAueHk7CglmbG9hdCBncmFkX2RvdCA9IGRvdChncmFkLCBncmFkKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjE3NTVlLTM4KTsKCWZsb2F0IGludmxlbiA9IGludmVyc2VzcXJ0KGdyYWRfZG90KTsKCWZsb2F0IGVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNS10ZXN0Kmludmxlbik7CglvZmZzZXQgPSB2RWxsaXBzZU9mZnNldHNfUzAueHkqdkVsbGlwc2VSYWRpaV9TMC56dzsKCXRlc3QgPSBkb3Qob2Zmc2V0LCBvZmZzZXQpIC0gMS4wOwoJZ3JhZCA9IDIuMCpvZmZzZXQqdkVsbGlwc2VSYWRpaV9TMC56dzsKCWdyYWRfZG90ID0gZG90KGdyYWQsIGdyYWQpOwoJaW52bGVuID0gaW52ZXJzZXNxcnQoZ3JhZF9kb3QpOwoJZWRnZUFscGhhICo9IHNhdHVyYXRlKDAuNSt0ZXN0Kmludmxlbik7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGhhbGYoZWRnZUFscGhhKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpbkVsbGlwc2VPZmZzZXQADgAAAGluRWxsaXBzZVJhZGlpAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAB5AAAAACQHEB4XIQAQAADQAAAABAAAAAAABAEMVDOMCJKRAAAAAHAAAAAAAAAAACQAGAAAAAQAAAAAAAQQGAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAABRBQAAY29uc3QgaW50IGtGaWxsQUFfUzFfYzAgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKaGFsZjQgQ2lyY2xlX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZiBkOwoJaWYgKGludCgzKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzApIAoJewoJCWQgPSBoYWxmKChsZW5ndGgoKHVjaXJjbGVfUzFfYzAueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMV9jMC53KSAtIDEuMCkgKiB1Y2lyY2xlX1MxX2MwLnopOwoJfQoJZWxzZSAKCXsKCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1MxX2MwLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfUzFfYzAudykpICogdWNpcmNsZV9TMV9jMC56KTsKCX0KCXJldHVybiBoYWxmNChoYWxmNChpbnQoMykgPT0ga0ZpbGxBQV9TMV9jMCB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzFfYzAgPyBzYXR1cmF0ZShkKSA6IGhhbGYoZCA+IDAuNSA/IDEgOiAwKSkpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoX3NyYywgQ2lyY2xlX1MxX2MwKF9zcmMpKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IHhfcGx1c18xPXZhcmNjb29yZF9TMC54LCB5PXZhcmNjb29yZF9TMC55OwoJaGFsZiBjb3ZlcmFnZTsKCWlmICgwID09IHhfcGx1c18xKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoeSk7Cgl9CgllbHNlIAoJewoJCWZsb2F0IGZuID0geF9wbHVzXzEgKiAoeF9wbHVzXzEgLSAyKTsKCQlmbiA9IGZtYSh5LHksIGZuKTsKCQlmbG9hdCBmbndpZHRoID0gZndpZHRoKGZuKTsKCQljb3ZlcmFnZSA9IC41IC0gaGFsZihmbi9mbndpZHRoKTsKCQljb3ZlcmFnZSA9IGNsYW1wKGNvdmVyYWdlLCAwLCAxKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoY292ZXJhZ2UpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q292ZXJhZ2VfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","HTQAAGAABBYAAAEIXBAAAGEAMAAAAAAAAAAAAAAAQAHAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M/AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgABAAAABQMAAGluIGZsb2F0NCB2UXVhZEVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGYgZWRnZUFscGhhOwoJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1MwLnosIHZRdWFkRWRnZV9TMC53KSArIDAuNSwgMS4wKSk7Cgl9CgllbHNlIAoJewoJCWhhbGYyIGdGID0gaGFsZjIoaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TMC54KnZRdWFkRWRnZV9TMC54IC0gdlF1YWRFZGdlX1MwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAAAAAA=","HVJAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAABAAAAAABBAMABAAOAAAABAAAAAAABBAMAAA":"CgAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAADQAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfUzApKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAD2AAAAAAQAVSWGRIBAAADAAAAACAAAAAAQCGEIQOZLBIQAAAABQAAAAAAAAAAAAFAAMAAAABAAAAAAABBAMAAA":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAARQUAAGNvbnN0IGludCBrRmlsbEJXX1MxX2MwID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxX2MwID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxX2MwID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IFJlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGNvdmVyYWdlOwoJaWYgKGludCgxKSA9PSBrRmlsbEJXX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMV9jMCkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fUzFfYzAuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1MxX2MwLnh5LCBza19GcmFnQ29vcmQueHkpKSkpOwoJfQoJZWxzZSAKCXsKCQloYWxmNCBkaXN0czQgPSBzYXR1cmF0ZShoYWxmNCgxLjAsIDEuMCwgLTEuMCwgLTEuMCkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIHVyZWN0VW5pZm9ybV9TMV9jMCkpOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCWlmIChpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzFfYzAgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEFBX1MxX2MwKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIGhhbGY0KGhhbGY0KGNvdmVyYWdlKSk7Cn0KaGFsZjQgQmxlbmRfUzEoaGFsZjQgX3NyYywgaGFsZjQgX2RzdCkgCnsKCXJldHVybiBibGVuZF9tb2R1bGF0ZShSZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvdmVyYWdlX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIBAIAAAABLCIIBAAAAABAEGABBAMAACAIAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADJAwAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfUzFfYzBfYzAueHksIHVjbGFtcF9TMV9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoTWF0cml4RWZmZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvbG9yX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAKPABAAAAAAB2AAAAAAACAAAAAEBSAAAAAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAACbBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpoYWxmNCBFbGxpcHRpY2FsUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CglmbG9hdDIgWiA9IGR4eSAqIHVpbnZSYWRpaVhZX1MxLnh5OwoJaGFsZiBpbXBsaWNpdCA9IGhhbGYoZG90KFosIGR4eSkgLSAxLjApOwoJaGFsZiBncmFkX2RvdCA9IGhhbGYoNC4wICogZG90KFosIFopKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjBlLTQpOwoJaGFsZiBhcHByb3hfZGlzdCA9IGltcGxpY2l0ICogaGFsZihpbnZlcnNlc3FydChncmFkX2RvdCkpOwoJaGFsZiBhbHBoYSA9IGNsYW1wKDAuNSArIGFwcHJveF9kaXN0LCAwLjAsIDEuMCk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEVsbGlwdGljYWxSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGAAZAADIAAAACU53QJEKAAAAAAMAAAAAIAAAAAAGIRDFB2XASAUAABQAAAAAAAAAAAAADUAAAAAAAEAAAAAIDEAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAACrBAAAY29uc3QgaW50IGtGaWxsQUFfUzFfYzAgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzFfYzAgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzFfYzAgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpoYWxmNCBDaXJjbGVfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGQ7CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMV9jMCkgCgl7CgkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TMV9jMC54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxX2MwLncpIC0gMS4wKSAqIHVjaXJjbGVfUzFfYzAueik7Cgl9CgllbHNlIAoJewoJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfUzFfYzAueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMV9jMC53KSkgKiB1Y2lyY2xlX1MxX2MwLnopOwoJfQoJcmV0dXJuIGhhbGY0KGhhbGY0KGludCgxKSA9PSBrRmlsbEFBX1MxX2MwIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMV9jMCA/IHNhdHVyYXRlKGQpIDogaGFsZihkID4gMC41ID8gMSA6IDApKSk7Cn0KaGFsZjQgQmxlbmRfUzEoaGFsZjQgX3NyYywgaGFsZjQgX2RzdCkgCnsKCXJldHVybiBibGVuZF9tb2R1bGF0ZShfc3JjLCBDaXJjbGVfUzFfYzAoX3NyYykpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogaGFsZjQoMSkpKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb3ZlcmFnZV9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAABAEAAAABJYQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAC8GAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TMV9jMF9jMF9jMC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAADqAQAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","BYIBQAAABQAAIAABBYAAAEIXBAAP777777777777AAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1M+AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzNfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAABgEAAGluIGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAFBQATAAAAAAFAAMAAAABAAAAAAABBAMAAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAABABAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgRWxsaXB0aWNhbFJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJZmxvYXQyIFogPSBkeHkgKiB1aW52UmFkaWlYWV9TMS54eTsKCWhhbGYgaW1wbGljaXQgPSBoYWxmKGRvdChaLCBkeHkpIC0gMS4wKTsKCWhhbGYgZ3JhZF9kb3QgPSBoYWxmKDQuMCAqIGRvdChaLCBaKSk7CglncmFkX2RvdCA9IG1heChncmFkX2RvdCwgMS4wZS00KTsKCWhhbGYgYXBwcm94X2Rpc3QgPSBpbXBsaWNpdCAqIGhhbGYoaW52ZXJzZXNxcnQoZ3JhZF9kb3QpKTsKCWhhbGYgYWxwaGEgPSBjbGFtcCgwLjUgKyBhcHByb3hfZGlzdCwgMC4wLCAxLjApOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRWxsaXB0aWNhbFJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAAAAAA==","AYTRVAADQAAAOAEARAFQJAABBADAAAILBYAACCYUQD777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7CmluIGhhbGYzIGluQ2xpcFBsYW5lOwppbiBoYWxmMyBpbklzZWN0UGxhbmU7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKb3V0IGhhbGYzIHZpbklzZWN0UGxhbmVfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5DbGlwUGxhbmVfUzAgPSBpbkNsaXBQbGFuZTsKCXZpbklzZWN0UGxhbmVfUzAgPSBpbklzZWN0UGxhbmU7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1MwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TMC55dzsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAADdAwAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKaW4gaGFsZjMgdmluSXNlY3RQbGFuZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGYzIGNsaXBQbGFuZTsKCWNsaXBQbGFuZSA9IHZpbkNsaXBQbGFuZV9TMDsKCWhhbGYzIGlzZWN0UGxhbmU7Cglpc2VjdFBsYW5lID0gdmluSXNlY3RQbGFuZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGYgY2xpcCA9IGhhbGYoc2F0dXJhdGUoY2lyY2xlRWRnZS56ICogZG90KGNpcmNsZUVkZ2UueHksIGNsaXBQbGFuZS54eSkgKyBjbGlwUGxhbmUueikpOwoJY2xpcCAqPSBoYWxmKHNhdHVyYXRlKGNpcmNsZUVkZ2UueiAqIGRvdChjaXJjbGVFZGdlLnh5LCBpc2VjdFBsYW5lLnh5KSArIGlzZWN0UGxhbmUueikpOwoJZWRnZUFscGhhICo9IGNsaXA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlCwAAAGluQ2xpcFBsYW5lAAwAAABpbklzZWN0UGxhbmUAAAAA","DBAAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAIAAAAAAAAAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1NVAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludDIgY29vcmRzID0gaW50MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJaW50IHRleElkeCA9IGNvb3Jkcy54ID4+IDEzOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGNvb3Jkcy54ICYgMHgxRkZGLCBjb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAAEICAAB1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzFfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7CglpZiAodlRleEluZGV4X1MwID09IDApIAoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWVsc2UgCgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzFfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","B2AAQAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CgAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cgl2aW5Db3ZlcmFnZV9TMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAATQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdUNvbG9yX1MwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAAAAAAA","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAACbAgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAAAAAA=","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAB7AgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAIAAEAAAABJYQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAC8GAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TMV9jMF9jMF9jMC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueik7CgljbGFtcGVkQ29vcmQueSA9IHN1YnNldENvb3JkLnk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","EADQAAAAAEAAAAAUAABQAAQPAAABCFYMAAKAUEAAAAAAAAABAAAAAAAAAAANAAIAAAABAAAAACAJAAIAAAAA":"CgAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7CmluIGZsb2F0MyBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgZmxvYXQyIHZJbnRUZXh0dXJlQ29vcmRzX1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJdkludFRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkczsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MyBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGluUG9zaXRpb24ueHkwejsKfQoAAAAAAACfAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEaXN0YW5jZUZpZWxkUGF0aAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfUzA7CgloYWxmNCB0ZXhDb2xvcjsKCXsKCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLnJycnI7Cgl9CgloYWxmIGRpc3RhbmNlID0gNy45Njg3NSoodGV4Q29sb3IuciAtIDAuNTAxOTYwNzg0MzEpOwoJaGFsZiBhZndpZHRoOwoJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeCh2SW50VGV4dHVyZUNvb3Jkc19TMC54KSkpOwoJaGFsZiB2YWwgPSBzbW9vdGhzdGVwKC1hZndpZHRoLCBhZndpZHRoLCBkaXN0YW5jZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KHZhbCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAAA5AAAAAAABAAAAACAZAAAAA":"CgAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgAAAABFAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2YXJjY29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAEQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAA3AwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAEAQAAAAGQCBAMQACAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CgAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAE0DAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMC54eSwgdWNsYW1wX1MxX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAAAAAA=","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAsQEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAAAAAA=","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAAoAIAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAHSADQAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CgAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAAQAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAAAAAA=","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBQU7BTXIAAAAAACAWXW3ZEQAAAADAAAAAGATHIBICYCAAAEBP2LBIPAAAAAIAAAAAEARRALJ3F5SMAAAABQAAAABABTUEURMBAAACAH5FYUHQAAAAAAAEAAAAAZ4RGGRCQFAEAAAAAAAAAGARP2LVJPAAAAAAAAEAAAABSKRXZFAUHQAAAAAAAAAACAA4AAAACAAAAAAACCAYAA":"CgAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAIIGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgMWUtMDUsIDEuMCwgMC4wLCAwLjApKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckxheW91dF9TMV9jMF9jMV9jMChfaW5wdXQpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzRfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoX3RtcF80X2luQ29sb3IpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIWJvb2woaW50KDEpKSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKF90bXBfNF9pbkNvbG9yLCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglyZXR1cm4gaGFsZjQob3V0Q29sb3IpOwp9CmhhbGY0IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEoaGFsZjQgX2lucHV0KSAKewoJX2lucHV0ID0gQ2xhbXBlZEdyYWRpZW50X1MxX2MwKF9pbnB1dCk7CgloYWxmNCBfdG1wXzVfaW5Db2xvciA9IF9pbnB1dDsKCXJldHVybiBoYWxmNChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CgAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAB5AwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UAAAAA","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CgAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAAEQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CgAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAuAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAAAAAA==","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CgAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAIBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdCB2Y292ZXJhZ2VfUzA7CmZsYXQgaW4gZmxvYXQ0IHZnZW9tU3Vic2V0X1MwOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAAAAAA=="}} \ No newline at end of file diff --git a/test/fake/device_service.dart b/test/fake/device_service.dart index c8afba4ec..911aff66f 100644 --- a/test/fake/device_service.dart +++ b/test/fake/device_service.dart @@ -4,5 +4,5 @@ import 'package:test/fake.dart'; class FakeDeviceService extends Fake implements DeviceService { @override - Future getDefaultTimeZone() => SynchronousFuture(''); + Future getDefaultTimeZoneRawOffsetMillis() => SynchronousFuture(3600000); } diff --git a/test/fake/media_store_service.dart b/test/fake/media_store_service.dart index 0649476ab..eb9082ee2 100644 --- a/test/fake/media_store_service.dart +++ b/test/fake/media_store_service.dart @@ -39,6 +39,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService { contentId ??= id; final date = dateSecs; return AvesEntry( + origin: EntryOrigins.mediaStoreContent, id: id, uri: 'content://media/external/images/media/$contentId', path: '$album/$filenameWithoutExtension.jpg', diff --git a/test/fake/metadata_db.dart b/test/fake/metadata_db.dart index 1d8fdc1a1..408ac2c3c 100644 --- a/test/fake/metadata_db.dart +++ b/test/fake/metadata_db.dart @@ -6,6 +6,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/metadata/address.dart'; import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/trash.dart'; +import 'package:aves/model/vaults/details.dart'; import 'package:flutter/foundation.dart'; import 'package:test/fake.dart'; @@ -19,26 +20,32 @@ class FakeMetadataDb extends Fake implements MetadataDb { Future init() => SynchronousFuture(null); @override - Future removeIds(Iterable ids, {Set? dataTypes}) => SynchronousFuture(null); + Future removeIds(Set ids, {Set? dataTypes}) => SynchronousFuture(null); // entries @override - Future> loadEntries({String? directory}) => SynchronousFuture({}); + Future> loadEntries({int? origin, String? directory}) => SynchronousFuture({}); @override - Future saveEntries(Iterable entries) => SynchronousFuture(null); + Future saveEntries(Set entries) => SynchronousFuture(null); @override Future updateEntry(int id, AvesEntry entry) => SynchronousFuture(null); // date taken + @override + Future clearDates() => SynchronousFuture(null); + @override Future> loadDates() => SynchronousFuture({}); // catalog metadata + @override + Future clearCatalogMetadata() => SynchronousFuture(null); + @override Future> loadCatalogMetadata() => SynchronousFuture({}); @@ -59,6 +66,11 @@ class FakeMetadataDb extends Fake implements MetadataDb { @override Future updateAddress(int id, AddressDetails? address) => SynchronousFuture(null); + // vaults + + @override + Future> loadAllVaults() => SynchronousFuture({}); + // trash @override @@ -76,13 +88,13 @@ class FakeMetadataDb extends Fake implements MetadataDb { Future> loadAllFavourites() => SynchronousFuture({}); @override - Future addFavourites(Iterable rows) => SynchronousFuture(null); + Future addFavourites(Set rows) => SynchronousFuture(null); @override Future updateFavouriteId(int id, FavouriteRow row) => SynchronousFuture(null); @override - Future removeFavourites(Iterable rows) => SynchronousFuture(null); + Future removeFavourites(Set rows) => SynchronousFuture(null); // covers @@ -90,7 +102,7 @@ class FakeMetadataDb extends Fake implements MetadataDb { Future> loadAllCovers() => SynchronousFuture({}); @override - Future addCovers(Iterable rows) => SynchronousFuture(null); + Future addCovers(Set rows) => SynchronousFuture(null); @override Future updateCoverEntryId(int id, CoverRow row) => SynchronousFuture(null); @@ -101,5 +113,5 @@ class FakeMetadataDb extends Fake implements MetadataDb { // video playback @override - Future removeVideoPlayback(Iterable ids) => SynchronousFuture(null); + Future removeVideoPlayback(Set ids) => SynchronousFuture(null); } diff --git a/test/fake/storage_service.dart b/test/fake/storage_service.dart index 49714a4b4..e74eacdcd 100644 --- a/test/fake/storage_service.dart +++ b/test/fake/storage_service.dart @@ -27,4 +27,7 @@ class FakeStorageService extends Fake implements StorageService { state: 'fake', ), }); + + @override + Future getVaultRoot() => SynchronousFuture('/vault/'); } diff --git a/test/model/collection_source_test.dart b/test/model/collection_source_test.dart index 033a3bdde..20edbce4c 100644 --- a/test/model/collection_source_test.dart +++ b/test/model/collection_source_test.dart @@ -26,6 +26,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; import 'package:path/path.dart' as p; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import '../fake/android_app_service.dart'; import '../fake/availability.dart'; @@ -66,6 +67,7 @@ void main() { getIt.registerLazySingleton(FakeStorageService.new); getIt.registerLazySingleton(FakeWindowService.new); + SharedPreferencesStorePlatform.instance = InMemorySharedPreferencesStore.empty(); await settings.init(monitorPlatformSettings: false); settings.canUseAnalysisService = false; await androidFileUtils.init(); diff --git a/untranslated.json b/untranslated.json index c87e286de..5cea5a17c 100644 --- a/untranslated.json +++ b/untranslated.json @@ -18,11 +18,14 @@ "chipActionFilterOut", "chipActionFilterIn", "chipActionHide", + "chipActionLock", "chipActionPin", "chipActionUnpin", "chipActionRename", "chipActionSetCover", "chipActionCreateAlbum", + "chipActionCreateVault", + "chipActionConfigureVault", "entryActionCopyToClipboard", "entryActionDelete", "entryActionConvert", @@ -57,7 +60,7 @@ "videoActionSkip10", "videoActionSelectStreams", "videoActionSetSpeed", - "videoActionSettings", + "viewerActionSettings", "slideshowActionResume", "slideshowActionShowInCollection", "entryInfoActionEditDate", @@ -91,6 +94,14 @@ "filterTypeGeotiffLabel", "filterMimeImageLabel", "filterMimeVideoLabel", + "accessibilityAnimationsRemove", + "accessibilityAnimationsKeep", + "albumTierNew", + "albumTierPinned", + "albumTierSpecial", + "albumTierApps", + "albumTierVaults", + "albumTierRegular", "coordinateFormatDms", "coordinateFormatDecimal", "coordinateDms", @@ -98,15 +109,12 @@ "coordinateDmsSouth", "coordinateDmsEast", "coordinateDmsWest", - "unitSystemMetric", - "unitSystemImperial", - "videoLoopModeNever", - "videoLoopModeShortOnly", - "videoLoopModeAlways", - "videoControlsPlay", - "videoControlsPlaySeek", - "videoControlsPlayOutside", - "videoControlsNone", + "displayRefreshRatePreferHighest", + "displayRefreshRatePreferLowest", + "keepScreenOnNever", + "keepScreenOnVideoPlayback", + "keepScreenOnViewerOnly", + "keepScreenOnAlways", "mapStyleGoogleNormal", "mapStyleGoogleHybrid", "mapStyleGoogleTerrain", @@ -118,22 +126,25 @@ "nameConflictStrategyRename", "nameConflictStrategyReplace", "nameConflictStrategySkip", - "keepScreenOnNever", - "keepScreenOnVideoPlayback", - "keepScreenOnViewerOnly", - "keepScreenOnAlways", - "accessibilityAnimationsRemove", - "accessibilityAnimationsKeep", - "displayRefreshRatePreferHighest", - "displayRefreshRatePreferLowest", "subtitlePositionTop", "subtitlePositionBottom", - "videoPlaybackSkip", - "videoPlaybackMuted", - "videoPlaybackWithSound", "themeBrightnessLight", "themeBrightnessDark", "themeBrightnessBlack", + "unitSystemMetric", + "unitSystemImperial", + "vaultLockTypePin", + "vaultLockTypePassword", + "videoControlsPlay", + "videoControlsPlaySeek", + "videoControlsPlayOutside", + "videoControlsNone", + "videoLoopModeNever", + "videoLoopModeShortOnly", + "videoLoopModeAlways", + "videoPlaybackSkip", + "videoPlaybackMuted", + "videoPlaybackWithSound", "viewerTransitionSlide", "viewerTransitionParallax", "viewerTransitionFade", @@ -147,11 +158,6 @@ "widgetOpenPageHome", "widgetOpenPageCollection", "widgetOpenPageViewer", - "albumTierNew", - "albumTierPinned", - "albumTierSpecial", - "albumTierApps", - "albumTierRegular", "storageVolumeDescriptionFallbackPrimary", "storageVolumeDescriptionFallbackNonPrimary", "rootDirectoryDescription", @@ -181,6 +187,18 @@ "newAlbumDialogNameLabel", "newAlbumDialogNameLabelAlreadyExistsHelper", "newAlbumDialogStorageLabel", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", "renameAlbumDialogLabel", "renameAlbumDialogLabelAlreadyExistsHelper", "renameEntrySetPageTitle", @@ -347,7 +365,6 @@ "albumVideoCaptures", "albumPageTitle", "albumEmpty", - "createAlbumTooltip", "createAlbumButtonLabel", "newFilterBanner", "countryPageTitle", @@ -391,6 +408,7 @@ "settingsConfirmationBeforeMoveToBinItems", "settingsConfirmationBeforeMoveUndatedItems", "settingsConfirmationAfterMoveToBinItems", + "settingsConfirmationVaultDataLoss", "settingsNavigationDrawerTile", "settingsNavigationDrawerEditorPageTitle", "settingsNavigationDrawerBanner", @@ -483,6 +501,7 @@ "settingsSaveSearchHistory", "settingsEnableBin", "settingsEnableBinSubtitle", + "settingsDisablingBinWarningDialogMessage", "settingsAllowMediaManagement", "settingsHiddenItemsTile", "settingsHiddenItemsPageTitle", @@ -572,34 +591,79 @@ ], "cs": [ - "filterLocatedLabel", - "filterTaggedLabel", - "tooManyItemsErrorDialogMessage", - "settingsModificationWarningDialogMessage", - "settingsViewerShowDescription", - "settingsVideoGestureVerticalDragBrightnessVolume", - "settingsDisplayUseTvInterface" + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", + "albumTierVaults", + "vaultLockTypePin", + "vaultLockTypePassword", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", + "settingsConfirmationVaultDataLoss", + "settingsDisablingBinWarningDialogMessage" ], "de": [ - "columnCount", - "filterLocatedLabel", - "filterTaggedLabel", - "tooManyItemsErrorDialogMessage", - "settingsModificationWarningDialogMessage", - "settingsViewerShowDescription", - "settingsVideoGestureVerticalDragBrightnessVolume", - "settingsAccessibilityShowPinchGestureAlternatives", - "settingsDisplayUseTvInterface" + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", + "albumTierVaults", + "vaultLockTypePin", + "vaultLockTypePassword", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", + "settingsConfirmationVaultDataLoss", + "settingsDisablingBinWarningDialogMessage" ], - "el": [ - "tooManyItemsErrorDialogMessage", - "settingsVideoGestureVerticalDragBrightnessVolume" + "eu": [ + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", + "albumTierVaults", + "vaultLockTypePin", + "vaultLockTypePassword", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", + "settingsConfirmationVaultDataLoss", + "settingsDisablingBinWarningDialogMessage" ], "fa": [ "clearTooltip", + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", "videoActionPause", "videoActionPlay", "videoActionSelectStreams", @@ -612,6 +676,13 @@ "filterTypeAnimatedLabel", "filterTypeRawLabel", "filterTypeGeotiffLabel", + "accessibilityAnimationsRemove", + "accessibilityAnimationsKeep", + "albumTierNew", + "albumTierPinned", + "albumTierSpecial", + "albumTierVaults", + "albumTierRegular", "coordinateFormatDms", "coordinateFormatDecimal", "coordinateDms", @@ -619,14 +690,14 @@ "coordinateDmsSouth", "coordinateDmsEast", "coordinateDmsWest", - "videoControlsNone", - "nameConflictStrategySkip", "keepScreenOnNever", "keepScreenOnVideoPlayback", "keepScreenOnViewerOnly", "keepScreenOnAlways", - "accessibilityAnimationsRemove", - "accessibilityAnimationsKeep", + "nameConflictStrategySkip", + "vaultLockTypePin", + "vaultLockTypePassword", + "videoControlsNone", "videoPlaybackSkip", "viewerTransitionSlide", "viewerTransitionParallax", @@ -641,10 +712,6 @@ "widgetOpenPageHome", "widgetOpenPageCollection", "widgetOpenPageViewer", - "albumTierNew", - "albumTierPinned", - "albumTierSpecial", - "albumTierRegular", "rootDirectoryDescription", "otherDirectoryDescription", "restrictedAccessDialogMessage", @@ -669,6 +736,18 @@ "newAlbumDialogNameLabel", "newAlbumDialogNameLabelAlreadyExistsHelper", "newAlbumDialogStorageLabel", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", "renameAlbumDialogLabel", "renameAlbumDialogLabelAlreadyExistsHelper", "renameEntrySetPageTitle", @@ -809,7 +888,6 @@ "albumVideoCaptures", "albumPageTitle", "albumEmpty", - "createAlbumTooltip", "createAlbumButtonLabel", "newFilterBanner", "countryPageTitle", @@ -849,6 +927,7 @@ "settingsConfirmationBeforeMoveToBinItems", "settingsConfirmationBeforeMoveUndatedItems", "settingsConfirmationAfterMoveToBinItems", + "settingsConfirmationVaultDataLoss", "settingsNavigationDrawerTile", "settingsNavigationDrawerEditorPageTitle", "settingsNavigationDrawerBanner", @@ -941,6 +1020,7 @@ "settingsSaveSearchHistory", "settingsEnableBin", "settingsEnableBinSubtitle", + "settingsDisablingBinWarningDialogMessage", "settingsAllowMediaManagement", "settingsHiddenItemsTile", "settingsHiddenItemsPageTitle", @@ -1042,6 +1122,9 @@ "gl": [ "columnCount", + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", "entryActionShareImageOnly", "entryActionShareVideoOnly", "entryInfoActionExportMetadata", @@ -1051,19 +1134,27 @@ "filterNoAddressLabel", "filterLocatedLabel", "filterTaggedLabel", - "keepScreenOnVideoPlayback", "accessibilityAnimationsRemove", "accessibilityAnimationsKeep", + "albumTierNew", + "albumTierPinned", + "albumTierSpecial", + "albumTierApps", + "albumTierVaults", + "albumTierRegular", "displayRefreshRatePreferHighest", "displayRefreshRatePreferLowest", + "keepScreenOnVideoPlayback", "subtitlePositionTop", "subtitlePositionBottom", - "videoPlaybackSkip", - "videoPlaybackMuted", - "videoPlaybackWithSound", "themeBrightnessLight", "themeBrightnessDark", "themeBrightnessBlack", + "vaultLockTypePin", + "vaultLockTypePassword", + "videoPlaybackSkip", + "videoPlaybackMuted", + "videoPlaybackWithSound", "viewerTransitionSlide", "viewerTransitionParallax", "viewerTransitionFade", @@ -1077,11 +1168,6 @@ "widgetOpenPageHome", "widgetOpenPageCollection", "widgetOpenPageViewer", - "albumTierNew", - "albumTierPinned", - "albumTierSpecial", - "albumTierApps", - "albumTierRegular", "storageVolumeDescriptionFallbackPrimary", "storageVolumeDescriptionFallbackNonPrimary", "rootDirectoryDescription", @@ -1111,6 +1197,18 @@ "newAlbumDialogNameLabel", "newAlbumDialogNameLabelAlreadyExistsHelper", "newAlbumDialogStorageLabel", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", "renameAlbumDialogLabel", "renameAlbumDialogLabelAlreadyExistsHelper", "renameEntrySetPageTitle", @@ -1277,7 +1375,6 @@ "albumVideoCaptures", "albumPageTitle", "albumEmpty", - "createAlbumTooltip", "createAlbumButtonLabel", "newFilterBanner", "countryPageTitle", @@ -1321,6 +1418,7 @@ "settingsConfirmationBeforeMoveToBinItems", "settingsConfirmationBeforeMoveUndatedItems", "settingsConfirmationAfterMoveToBinItems", + "settingsConfirmationVaultDataLoss", "settingsNavigationDrawerTile", "settingsNavigationDrawerEditorPageTitle", "settingsNavigationDrawerBanner", @@ -1413,6 +1511,7 @@ "settingsSaveSearchHistory", "settingsEnableBin", "settingsEnableBinSubtitle", + "settingsDisablingBinWarningDialogMessage", "settingsAllowMediaManagement", "settingsHiddenItemsTile", "settingsHiddenItemsPageTitle", @@ -1551,11 +1650,14 @@ "chipActionFilterOut", "chipActionFilterIn", "chipActionHide", + "chipActionLock", "chipActionPin", "chipActionUnpin", "chipActionRename", "chipActionSetCover", "chipActionCreateAlbum", + "chipActionCreateVault", + "chipActionConfigureVault", "entryActionCopyToClipboard", "entryActionDelete", "entryActionConvert", @@ -1590,7 +1692,7 @@ "videoActionSkip10", "videoActionSelectStreams", "videoActionSetSpeed", - "videoActionSettings", + "viewerActionSettings", "slideshowActionResume", "slideshowActionShowInCollection", "entryInfoActionEditDate", @@ -1624,6 +1726,14 @@ "filterTypeGeotiffLabel", "filterMimeImageLabel", "filterMimeVideoLabel", + "accessibilityAnimationsRemove", + "accessibilityAnimationsKeep", + "albumTierNew", + "albumTierPinned", + "albumTierSpecial", + "albumTierApps", + "albumTierVaults", + "albumTierRegular", "coordinateFormatDms", "coordinateFormatDecimal", "coordinateDms", @@ -1631,15 +1741,12 @@ "coordinateDmsSouth", "coordinateDmsEast", "coordinateDmsWest", - "unitSystemMetric", - "unitSystemImperial", - "videoLoopModeNever", - "videoLoopModeShortOnly", - "videoLoopModeAlways", - "videoControlsPlay", - "videoControlsPlaySeek", - "videoControlsPlayOutside", - "videoControlsNone", + "displayRefreshRatePreferHighest", + "displayRefreshRatePreferLowest", + "keepScreenOnNever", + "keepScreenOnVideoPlayback", + "keepScreenOnViewerOnly", + "keepScreenOnAlways", "mapStyleGoogleNormal", "mapStyleGoogleHybrid", "mapStyleGoogleTerrain", @@ -1651,22 +1758,25 @@ "nameConflictStrategyRename", "nameConflictStrategyReplace", "nameConflictStrategySkip", - "keepScreenOnNever", - "keepScreenOnVideoPlayback", - "keepScreenOnViewerOnly", - "keepScreenOnAlways", - "accessibilityAnimationsRemove", - "accessibilityAnimationsKeep", - "displayRefreshRatePreferHighest", - "displayRefreshRatePreferLowest", "subtitlePositionTop", "subtitlePositionBottom", - "videoPlaybackSkip", - "videoPlaybackMuted", - "videoPlaybackWithSound", "themeBrightnessLight", "themeBrightnessDark", "themeBrightnessBlack", + "unitSystemMetric", + "unitSystemImperial", + "vaultLockTypePin", + "vaultLockTypePassword", + "videoControlsPlay", + "videoControlsPlaySeek", + "videoControlsPlayOutside", + "videoControlsNone", + "videoLoopModeNever", + "videoLoopModeShortOnly", + "videoLoopModeAlways", + "videoPlaybackSkip", + "videoPlaybackMuted", + "videoPlaybackWithSound", "viewerTransitionSlide", "viewerTransitionParallax", "viewerTransitionFade", @@ -1680,11 +1790,6 @@ "widgetOpenPageHome", "widgetOpenPageCollection", "widgetOpenPageViewer", - "albumTierNew", - "albumTierPinned", - "albumTierSpecial", - "albumTierApps", - "albumTierRegular", "storageVolumeDescriptionFallbackPrimary", "storageVolumeDescriptionFallbackNonPrimary", "rootDirectoryDescription", @@ -1714,6 +1819,18 @@ "newAlbumDialogNameLabel", "newAlbumDialogNameLabelAlreadyExistsHelper", "newAlbumDialogStorageLabel", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", "renameAlbumDialogLabel", "renameAlbumDialogLabelAlreadyExistsHelper", "renameEntrySetPageTitle", @@ -1880,7 +1997,6 @@ "albumVideoCaptures", "albumPageTitle", "albumEmpty", - "createAlbumTooltip", "createAlbumButtonLabel", "newFilterBanner", "countryPageTitle", @@ -1924,6 +2040,7 @@ "settingsConfirmationBeforeMoveToBinItems", "settingsConfirmationBeforeMoveUndatedItems", "settingsConfirmationAfterMoveToBinItems", + "settingsConfirmationVaultDataLoss", "settingsNavigationDrawerTile", "settingsNavigationDrawerEditorPageTitle", "settingsNavigationDrawerBanner", @@ -2016,6 +2133,7 @@ "settingsSaveSearchHistory", "settingsEnableBin", "settingsEnableBinSubtitle", + "settingsDisablingBinWarningDialogMessage", "settingsAllowMediaManagement", "settingsHiddenItemsTile", "settingsHiddenItemsPageTitle", @@ -2118,24 +2236,63 @@ ], "it": [ - "settingsVideoGestureVerticalDragBrightnessVolume" + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", + "albumTierVaults", + "vaultLockTypePin", + "vaultLockTypePassword", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", + "settingsConfirmationVaultDataLoss", + "settingsDisablingBinWarningDialogMessage" ], "ja": [ "columnCount", "chipActionFilterIn", + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", "filterNoAddressLabel", "filterLocatedLabel", "filterTaggedLabel", + "albumTierVaults", "keepScreenOnVideoPlayback", "subtitlePositionTop", "subtitlePositionBottom", + "vaultLockTypePin", + "vaultLockTypePassword", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", "tooManyItemsErrorDialogMessage", "settingsModificationWarningDialogMessage", + "settingsConfirmationVaultDataLoss", "settingsViewerShowDescription", "settingsVideoGestureVerticalDragBrightnessVolume", + "settingsDisablingBinWarningDialogMessage", "settingsAccessibilityShowPinchGestureAlternatives", "settingsDisplayUseTvInterface", "settingsWidgetDisplayedItem" @@ -2143,23 +2300,65 @@ "lt": [ "columnCount", + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", "filterLocatedLabel", "filterTaggedLabel", + "albumTierVaults", "keepScreenOnVideoPlayback", + "vaultLockTypePin", + "vaultLockTypePassword", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", "tooManyItemsErrorDialogMessage", "settingsModificationWarningDialogMessage", + "settingsConfirmationVaultDataLoss", "settingsViewerShowDescription", "settingsVideoGestureVerticalDragBrightnessVolume", + "settingsDisablingBinWarningDialogMessage", "settingsAccessibilityShowPinchGestureAlternatives", "settingsDisplayUseTvInterface" ], "nb": [ - "settingsVideoGestureVerticalDragBrightnessVolume" + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", + "albumTierVaults", + "vaultLockTypePin", + "vaultLockTypePassword", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", + "settingsConfirmationVaultDataLoss", + "settingsDisablingBinWarningDialogMessage" ], "nl": [ "columnCount", + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", "entryActionShareImageOnly", "entryActionShareVideoOnly", "entryInfoActionExportMetadata", @@ -2169,18 +2368,35 @@ "filterNoAddressLabel", "filterLocatedLabel", "filterTaggedLabel", + "albumTierVaults", "keepScreenOnVideoPlayback", "subtitlePositionTop", "subtitlePositionBottom", + "vaultLockTypePin", + "vaultLockTypePassword", "widgetDisplayedItemRandom", "widgetDisplayedItemMostRecent", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", "tooManyItemsErrorDialogMessage", "settingsModificationWarningDialogMessage", + "settingsConfirmationVaultDataLoss", "settingsViewerShowRatingTags", "settingsViewerShowDescription", "settingsSubtitleThemeTextPositionTile", "settingsSubtitleThemeTextPositionDialogTitle", "settingsVideoGestureVerticalDragBrightnessVolume", + "settingsDisablingBinWarningDialogMessage", "settingsAccessibilityShowPinchGestureAlternatives", "settingsDisplayUseTvInterface", "settingsWidgetDisplayedItem" @@ -2189,16 +2405,34 @@ "nn": [ "columnCount", "sourceStateCataloguing", + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", "entryInfoActionRemoveLocation", "filterLocatedLabel", "filterNoLocationLabel", "filterTaggedLabel", "accessibilityAnimationsKeep", + "albumTierVaults", "displayRefreshRatePreferHighest", "displayRefreshRatePreferLowest", + "vaultLockTypePin", + "vaultLockTypePassword", "wallpaperTargetHome", "wallpaperTargetHomeLock", "setCoverDialogCustom", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", "editEntryDialogTargetFieldsHeader", "editEntryDateDialogSetCustom", "editEntryLocationDialogTitle", @@ -2301,7 +2535,6 @@ "albumVideoCaptures", "albumPageTitle", "albumEmpty", - "createAlbumTooltip", "createAlbumButtonLabel", "newFilterBanner", "countryPageTitle", @@ -2345,6 +2578,7 @@ "settingsConfirmationBeforeMoveToBinItems", "settingsConfirmationBeforeMoveUndatedItems", "settingsConfirmationAfterMoveToBinItems", + "settingsConfirmationVaultDataLoss", "settingsNavigationDrawerTile", "settingsNavigationDrawerEditorPageTitle", "settingsNavigationDrawerBanner", @@ -2437,6 +2671,7 @@ "settingsSaveSearchHistory", "settingsEnableBin", "settingsEnableBinSubtitle", + "settingsDisablingBinWarningDialogMessage", "settingsAllowMediaManagement", "settingsHiddenItemsTile", "settingsHiddenItemsPageTitle", @@ -2476,35 +2711,154 @@ "pt": [ "columnCount", + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", + "albumTierVaults", + "vaultLockTypePin", + "vaultLockTypePassword", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", "tooManyItemsErrorDialogMessage", - "settingsVideoGestureVerticalDragBrightnessVolume" - ], - - "ro": [ - "settingsVideoGestureVerticalDragBrightnessVolume" + "settingsConfirmationVaultDataLoss", + "settingsVideoGestureVerticalDragBrightnessVolume", + "settingsDisablingBinWarningDialogMessage" ], "ru": [ + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", "filterLocatedLabel", "filterTaggedLabel", + "albumTierVaults", + "vaultLockTypePin", + "vaultLockTypePassword", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", "tooManyItemsErrorDialogMessage", "settingsModificationWarningDialogMessage", + "settingsConfirmationVaultDataLoss", "settingsVideoGestureVerticalDragBrightnessVolume", + "settingsDisablingBinWarningDialogMessage", "settingsDisplayUseTvInterface" ], - "th": [ + "sk": [ "itemCount", "columnCount", "timeSeconds", - "timeMinutes", - "timeDays", - "focalLength", - "applyButtonLabel", - "editEntryDateDialogExtractFromTitle", - "editEntryDateDialogShift", + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", + "filterLocatedLabel", + "filterNoLocationLabel", + "albumTierVaults", + "coordinateDms", + "vaultLockTypePin", + "vaultLockTypePassword", + "otherDirectoryDescription", + "storageAccessDialogMessage", + "restrictedAccessDialogMessage", + "notEnoughSpaceDialogMessage", + "unsupportedTypeDialogMessage", + "binEntriesConfirmationDialogMessage", + "deleteEntriesConfirmationDialogMessage", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", + "deleteSingleAlbumConfirmationDialogMessage", + "deleteMultiAlbumConfirmationDialogMessage", + "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", + "aboutLicensesFlutterPluginsSectionTitle", + "aboutLicensesFlutterPackagesSectionTitle", + "aboutLicensesDartPackagesSectionTitle", + "aboutLicensesShowAllButtonLabel", + "policyPageTitle", + "collectionPageTitle", + "collectionPickPageTitle", + "collectionSelectPageTitle", "collectionActionShowTitleSearch", "collectionActionHideTitleSearch", "collectionActionAddShortcut", @@ -2582,7 +2936,6 @@ "albumVideoCaptures", "albumPageTitle", "albumEmpty", - "createAlbumTooltip", "createAlbumButtonLabel", "newFilterBanner", "countryPageTitle", @@ -2626,6 +2979,7 @@ "settingsConfirmationBeforeMoveToBinItems", "settingsConfirmationBeforeMoveUndatedItems", "settingsConfirmationAfterMoveToBinItems", + "settingsConfirmationVaultDataLoss", "settingsNavigationDrawerTile", "settingsNavigationDrawerEditorPageTitle", "settingsNavigationDrawerBanner", @@ -2718,6 +3072,7 @@ "settingsSaveSearchHistory", "settingsEnableBin", "settingsEnableBinSubtitle", + "settingsDisablingBinWarningDialogMessage", "settingsAllowMediaManagement", "settingsHiddenItemsTile", "settingsHiddenItemsPageTitle", @@ -2819,25 +3174,433 @@ "filePickerUseThisFolder" ], + "th": [ + "itemCount", + "columnCount", + "timeSeconds", + "timeMinutes", + "timeDays", + "focalLength", + "applyButtonLabel", + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", + "albumTierVaults", + "vaultLockTypePin", + "vaultLockTypePassword", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", + "editEntryDateDialogExtractFromTitle", + "editEntryDateDialogShift", + "removeEntryMetadataDialogTitle", + "tooManyItemsErrorDialogMessage", + "collectionActionShowTitleSearch", + "collectionActionHideTitleSearch", + "collectionActionAddShortcut", + "collectionActionEmptyBin", + "collectionActionCopy", + "collectionActionMove", + "collectionActionRescan", + "collectionActionEdit", + "collectionSearchTitlesHintText", + "collectionGroupAlbum", + "collectionGroupMonth", + "collectionGroupDay", + "collectionGroupNone", + "sectionUnknown", + "dateToday", + "dateYesterday", + "dateThisMonth", + "collectionDeleteFailureFeedback", + "collectionCopyFailureFeedback", + "collectionMoveFailureFeedback", + "collectionRenameFailureFeedback", + "collectionEditFailureFeedback", + "collectionExportFailureFeedback", + "collectionCopySuccessFeedback", + "collectionMoveSuccessFeedback", + "collectionRenameSuccessFeedback", + "collectionEditSuccessFeedback", + "collectionEmptyFavourites", + "collectionEmptyVideos", + "collectionEmptyImages", + "collectionEmptyGrantAccessButtonLabel", + "collectionSelectSectionTooltip", + "collectionDeselectSectionTooltip", + "drawerAboutButton", + "drawerSettingsButton", + "drawerCollectionAll", + "drawerCollectionFavourites", + "drawerCollectionImages", + "drawerCollectionVideos", + "drawerCollectionAnimated", + "drawerCollectionMotionPhotos", + "drawerCollectionPanoramas", + "drawerCollectionRaws", + "drawerCollectionSphericalVideos", + "drawerAlbumPage", + "drawerCountryPage", + "drawerTagPage", + "sortByDate", + "sortByName", + "sortByItemCount", + "sortBySize", + "sortByAlbumFileName", + "sortByRating", + "sortOrderNewestFirst", + "sortOrderOldestFirst", + "sortOrderAtoZ", + "sortOrderZtoA", + "sortOrderHighestFirst", + "sortOrderLowestFirst", + "sortOrderLargestFirst", + "sortOrderSmallestFirst", + "albumGroupTier", + "albumGroupType", + "albumGroupVolume", + "albumGroupNone", + "albumMimeTypeMixed", + "albumPickPageTitleCopy", + "albumPickPageTitleExport", + "albumPickPageTitleMove", + "albumPickPageTitlePick", + "albumCamera", + "albumDownload", + "albumScreenshots", + "albumScreenRecordings", + "albumVideoCaptures", + "albumPageTitle", + "albumEmpty", + "createAlbumButtonLabel", + "newFilterBanner", + "countryPageTitle", + "countryEmpty", + "tagPageTitle", + "tagEmpty", + "binPageTitle", + "searchCollectionFieldHint", + "searchRecentSectionTitle", + "searchDateSectionTitle", + "searchAlbumsSectionTitle", + "searchCountriesSectionTitle", + "searchPlacesSectionTitle", + "searchTagsSectionTitle", + "searchRatingSectionTitle", + "searchMetadataSectionTitle", + "settingsPageTitle", + "settingsSystemDefault", + "settingsDefault", + "settingsDisabled", + "settingsModificationWarningDialogMessage", + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsActionExport", + "settingsActionExportDialogTitle", + "settingsActionImport", + "settingsActionImportDialogTitle", + "appExportCovers", + "appExportFavourites", + "appExportSettings", + "settingsNavigationSectionTitle", + "settingsHomeTile", + "settingsHomeDialogTitle", + "settingsShowBottomNavigationBar", + "settingsKeepScreenOnTile", + "settingsKeepScreenOnDialogTitle", + "settingsDoubleBackExit", + "settingsConfirmationTile", + "settingsConfirmationDialogTitle", + "settingsConfirmationBeforeDeleteItems", + "settingsConfirmationBeforeMoveToBinItems", + "settingsConfirmationBeforeMoveUndatedItems", + "settingsConfirmationAfterMoveToBinItems", + "settingsConfirmationVaultDataLoss", + "settingsNavigationDrawerTile", + "settingsNavigationDrawerEditorPageTitle", + "settingsNavigationDrawerBanner", + "settingsNavigationDrawerTabTypes", + "settingsNavigationDrawerTabAlbums", + "settingsNavigationDrawerTabPages", + "settingsNavigationDrawerAddAlbum", + "settingsThumbnailSectionTitle", + "settingsThumbnailOverlayTile", + "settingsThumbnailOverlayPageTitle", + "settingsThumbnailShowFavouriteIcon", + "settingsThumbnailShowTagIcon", + "settingsThumbnailShowLocationIcon", + "settingsThumbnailShowMotionPhotoIcon", + "settingsThumbnailShowRating", + "settingsThumbnailShowRawIcon", + "settingsThumbnailShowVideoDuration", + "settingsCollectionQuickActionsTile", + "settingsCollectionQuickActionEditorPageTitle", + "settingsCollectionQuickActionTabBrowsing", + "settingsCollectionQuickActionTabSelecting", + "settingsCollectionBrowsingQuickActionEditorBanner", + "settingsCollectionSelectionQuickActionEditorBanner", + "settingsViewerSectionTitle", + "settingsViewerGestureSideTapNext", + "settingsViewerUseCutout", + "settingsViewerMaximumBrightness", + "settingsMotionPhotoAutoPlay", + "settingsImageBackground", + "settingsViewerQuickActionsTile", + "settingsViewerQuickActionEditorPageTitle", + "settingsViewerQuickActionEditorBanner", + "settingsViewerQuickActionEditorDisplayedButtonsSectionTitle", + "settingsViewerQuickActionEditorAvailableButtonsSectionTitle", + "settingsViewerQuickActionEmpty", + "settingsViewerOverlayTile", + "settingsViewerOverlayPageTitle", + "settingsViewerShowOverlayOnOpening", + "settingsViewerShowMinimap", + "settingsViewerShowInformation", + "settingsViewerShowInformationSubtitle", + "settingsViewerShowRatingTags", + "settingsViewerShowShootingDetails", + "settingsViewerShowDescription", + "settingsViewerShowOverlayThumbnails", + "settingsViewerEnableOverlayBlurEffect", + "settingsViewerSlideshowTile", + "settingsViewerSlideshowPageTitle", + "settingsSlideshowRepeat", + "settingsSlideshowShuffle", + "settingsSlideshowFillScreen", + "settingsSlideshowAnimatedZoomEffect", + "settingsSlideshowTransitionTile", + "settingsSlideshowIntervalTile", + "settingsSlideshowVideoPlaybackTile", + "settingsSlideshowVideoPlaybackDialogTitle", + "settingsVideoPageTitle", + "settingsVideoSectionTitle", + "settingsVideoShowVideos", + "settingsVideoEnableHardwareAcceleration", + "settingsVideoAutoPlay", + "settingsVideoLoopModeTile", + "settingsVideoLoopModeDialogTitle", + "settingsSubtitleThemeTile", + "settingsSubtitleThemePageTitle", + "settingsSubtitleThemeSample", + "settingsSubtitleThemeTextAlignmentTile", + "settingsSubtitleThemeTextAlignmentDialogTitle", + "settingsSubtitleThemeTextPositionTile", + "settingsSubtitleThemeTextPositionDialogTitle", + "settingsSubtitleThemeTextSize", + "settingsSubtitleThemeShowOutline", + "settingsSubtitleThemeTextColor", + "settingsSubtitleThemeTextOpacity", + "settingsSubtitleThemeBackgroundColor", + "settingsSubtitleThemeBackgroundOpacity", + "settingsSubtitleThemeTextAlignmentLeft", + "settingsSubtitleThemeTextAlignmentCenter", + "settingsSubtitleThemeTextAlignmentRight", + "settingsVideoControlsTile", + "settingsVideoControlsPageTitle", + "settingsVideoButtonsTile", + "settingsVideoGestureDoubleTapTogglePlay", + "settingsVideoGestureSideDoubleTapSeek", + "settingsVideoGestureVerticalDragBrightnessVolume", + "settingsPrivacySectionTitle", + "settingsAllowInstalledAppAccess", + "settingsAllowInstalledAppAccessSubtitle", + "settingsAllowErrorReporting", + "settingsSaveSearchHistory", + "settingsEnableBin", + "settingsEnableBinSubtitle", + "settingsDisablingBinWarningDialogMessage", + "settingsAllowMediaManagement", + "settingsHiddenItemsTile", + "settingsHiddenItemsPageTitle", + "settingsHiddenItemsTabFilters", + "settingsHiddenFiltersBanner", + "settingsHiddenFiltersEmpty", + "settingsHiddenItemsTabPaths", + "settingsHiddenPathsBanner", + "addPathTooltip", + "settingsStorageAccessTile", + "settingsStorageAccessPageTitle", + "settingsStorageAccessBanner", + "settingsStorageAccessEmpty", + "settingsStorageAccessRevokeTooltip", + "settingsAccessibilitySectionTitle", + "settingsRemoveAnimationsTile", + "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", + "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", + "tagPlaceholderCountry", + "tagPlaceholderPlace", + "panoramaEnableSensorControl", + "panoramaDisableSensorControl", + "sourceViewerPageTitle", + "filePickerShowHiddenFiles", + "filePickerDoNotShowHiddenFiles", + "filePickerOpenFrom", + "filePickerNoItems", + "filePickerUseThisFolder" + ], + + "tr": [ + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", + "albumTierVaults", + "vaultLockTypePin", + "vaultLockTypePassword", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", + "settingsConfirmationVaultDataLoss", + "settingsDisablingBinWarningDialogMessage" + ], + "zh": [ + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", "filterLocatedLabel", "filterTaggedLabel", + "albumTierVaults", + "vaultLockTypePin", + "vaultLockTypePassword", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", "tooManyItemsErrorDialogMessage", "settingsModificationWarningDialogMessage", + "settingsConfirmationVaultDataLoss", "settingsViewerShowDescription", "settingsVideoGestureVerticalDragBrightnessVolume", + "settingsDisablingBinWarningDialogMessage", "settingsAccessibilityShowPinchGestureAlternatives", "settingsDisplayUseTvInterface" ], "zh_Hant": [ "columnCount", + "chipActionLock", + "chipActionCreateVault", + "chipActionConfigureVault", "filterLocatedLabel", "filterTaggedLabel", + "albumTierVaults", + "vaultLockTypePin", + "vaultLockTypePassword", + "newVaultWarningDialogMessage", + "newVaultDialogTitle", + "configureVaultDialogTitle", + "vaultDialogLockModeWhenScreenOff", + "vaultDialogLockTypeLabel", + "pinDialogEnter", + "pinDialogConfirm", + "passwordDialogEnter", + "passwordDialogConfirm", + "authenticateToConfigureVault", + "authenticateToUnlockVault", + "vaultBinUsageDialogMessage", "tooManyItemsErrorDialogMessage", "settingsModificationWarningDialogMessage", + "settingsConfirmationVaultDataLoss", "settingsViewerShowDescription", "settingsVideoGestureVerticalDragBrightnessVolume", + "settingsDisablingBinWarningDialogMessage", "settingsAccessibilityShowPinchGestureAlternatives", "settingsDisplayUseTvInterface" ] diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index 7683711d3..d1e8ac1c4 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,5 +1,5 @@ -In v1.7.10: +In v1.8.0: - Android TV support (cont'd) -- interact with videos via media session controls -- enjoy the app in Czech & Polish +- hide your secrets in vaults +- enjoy the app in Basque Full changelog available on GitHub \ No newline at end of file