diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a7501e099..a4fd88b54 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -17,7 +17,7 @@ jobs: # Available versions may lag behind https://github.com/flutter/flutter.git - uses: subosito/flutter-action@v2 with: - flutter-version: '3.3.2' + flutter-version: '3.3.4' channel: 'stable' - name: Clone the repository. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 659f91280..92122dc20 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: # Available versions may lag behind https://github.com/flutter/flutter.git - uses: subosito/flutter-action@v2 with: - flutter-version: '3.3.2' + flutter-version: '3.3.4' channel: 'stable' # Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1): @@ -56,15 +56,15 @@ jobs: rm release.keystore.asc mkdir outputs (cd scripts/; ./apply_flavor_play.sh) - flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.3.2.sksl.json + flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.3.4.sksl.json cp build/app/outputs/bundle/playRelease/*.aab outputs - flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.3.2.sksl.json + flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.3.4.sksl.json cp build/app/outputs/apk/play/release/*.apk outputs (cd scripts/; ./apply_flavor_huawei.sh) - flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_3.3.2.sksl.json + flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_3.3.4.sksl.json cp build/app/outputs/apk/huawei/release/*.apk outputs (cd scripts/; ./apply_flavor_izzy.sh) - flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_3.3.2.sksl.json + flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_3.3.4.sksl.json cp build/app/outputs/apk/izzy/release/*.apk outputs rm $AVES_STORE_FILE env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a258bf65..0dea5d4bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [v1.7.1] - 2022-10-09 + +### Added + +- mosaic layout +- reverse filters to filter out/in +- Collection: selection edit actions available as quick actions +- Albums: group by content type +- Info: improved display for XMP +- Stats: top albums +- Stats: open full top listings +- Video: option for muted auto play +- Slideshow / Screen saver: option for no transition +- Slideshow / Screen saver: animated zoom effect +- Widget: tap action setting +- Wallpaper: scroll effect option + +### Changed + +- upgraded Flutter to stable v3.3.4 + +### Fixed + +- restoring to missing Download subdir +- crash when cataloguing PNG with large chunks + ## [v1.7.0] - 2022-09-19 ### Added diff --git a/android/app/build.gradle b/android/app/build.gradle index 7347da8c8..a26f23c51 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,22 +155,22 @@ repositories { dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' implementation 'androidx.core:core-ktx:1.9.0' - implementation 'androidx.exifinterface:exifinterface:1.3.3' + implementation 'androidx.exifinterface:exifinterface:1.3.4' implementation 'androidx.multidex:multidex:2.0.1' implementation 'com.caverock:androidsvg-aar:1.4' - implementation 'com.commonsware.cwac:document:0.4.1' + implementation 'com.commonsware.cwac:document:0.5.0' implementation 'com.drewnoakes:metadata-extractor:2.18.0' // forked, built by JitPack, cf https://jitpack.io/p/deckerst/Android-TiffBitmapFactory implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a' // forked, built by JitPack, cf https://jitpack.io/p/deckerst/pixymeta-android implementation 'com.github.deckerst:pixymeta-android:706bd73d6e' - implementation 'com.github.bumptech.glide:glide:4.13.2' + implementation 'com.github.bumptech.glide:glide:4.14.2' // huawei flavor only - huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.5.2.300' + huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.7.2.300' - kapt 'androidx.annotation:annotation:1.4.0' - kapt 'com.github.bumptech.glide:compiler:4.13.0' + kapt 'androidx.annotation:annotation:1.5.0' + kapt 'com.github.bumptech.glide:compiler:4.14.2' compileOnly rootProject.findProject(':streams_channel') } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 28fa877f6..5e4b6da99 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,10 +1,10 @@ platform -> dart - // - need Context - MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this)) - MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this)) - MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) - MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) - MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this)) - - // result streaming: dart -> platform ->->-> dart - // - need Context - StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) } - StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) } - - // channel for service management - backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply { - setMethodCallHandler(context) - } + initChannels(this) HandlerThread("Analysis service handler", Process.THREAD_PRIORITY_BACKGROUND).apply { start() @@ -94,7 +73,36 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() { return START_NOT_STICKY } - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + private fun detachAndStop() { + analysisServiceBinder.detach() + stopSelf() + } + + private fun initChannels(context: Context) { + val messenger = flutterEngine!!.dartExecutor + + // channels for analysis + + // dart -> platform -> dart + // - need Context + MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context)) + MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(context)) + MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context)) + MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(context)) + MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(context)) + + // result streaming: dart -> platform ->->-> dart + // - need Context + StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(context, args) } + StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(context, args) } + + // channel for service management + backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply { + setMethodCallHandler { call, result -> onMethodCall(call, result) } + } + } + + private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "initialized" -> { Log.d(LOG_TAG, "background channel is ready") @@ -119,11 +127,6 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() { } } - private fun detachAndStop() { - analysisServiceBinder.detach() - stopSelf() - } - private fun buildNotification(title: String? = null, message: String? = null): Notification { val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetConfigureActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetConfigureActivity.kt index 70c55dbaf..a7026b9e1 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetConfigureActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetConfigureActivity.kt @@ -3,7 +3,6 @@ package deckers.thibault.aves import android.appwidget.AppWidgetManager import android.content.Intent import android.os.Bundle -import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel class HomeWidgetSettingsActivity : MainActivity() { @@ -26,7 +25,7 @@ class HomeWidgetSettingsActivity : MainActivity() { } val messenger = flutterEngine!!.dartExecutor - MethodChannel(messenger, CHANNEL).setMethodCallHandler { call: MethodCall, result: MethodChannel.Result -> + MethodChannel(messenger, CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "configure" -> { result.success(null) 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 4da0d671c..307eadc55 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt @@ -5,6 +5,8 @@ import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider import android.content.Context import android.content.Intent +import android.content.res.Configuration +import android.content.res.Resources import android.graphics.Bitmap import android.net.Uri import android.os.Build @@ -62,6 +64,18 @@ class HomeWidgetProvider : AppWidgetProvider() { } } + private fun getDevicePixelRatio(): Float = Resources.getSystem().displayMetrics.density + + private fun getWidgetSizePx(context: Context, widgetInfo: Bundle): Pair { + val devicePixelRatio = getDevicePixelRatio() + val isPortrait = context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT + val widthKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH else AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH + val heightKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT else AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT + val widthPx = (widgetInfo.getInt(widthKey) * devicePixelRatio).roundToInt() + val heightPx = (widgetInfo.getInt(heightKey) * devicePixelRatio).roundToInt() + return Pair(widthPx, heightPx) + } + private suspend fun getBytes( context: Context, widgetId: Int, @@ -69,9 +83,7 @@ class HomeWidgetProvider : AppWidgetProvider() { drawEntryImage: Boolean, reuseEntry: Boolean = false, ): ByteArray? { - val devicePixelRatio = context.resources.displayMetrics.density - val widthPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) * devicePixelRatio).roundToInt() - val heightPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) * devicePixelRatio).roundToInt() + val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo) if (widthPx == 0 || heightPx == 0) return null initFlutterEngine(context) @@ -85,7 +97,7 @@ class HomeWidgetProvider : AppWidgetProvider() { "widgetId" to widgetId, "widthPx" to widthPx, "heightPx" to heightPx, - "devicePixelRatio" to devicePixelRatio, + "devicePixelRatio" to getDevicePixelRatio(), "drawEntryImage" to drawEntryImage, "reuseEntry" to reuseEntry, ), object : MethodChannel.Result { @@ -120,9 +132,8 @@ class HomeWidgetProvider : AppWidgetProvider() { ) { bytes ?: return - val devicePixelRatio = context.resources.displayMetrics.density - val widthPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) * devicePixelRatio).roundToInt() - val heightPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) * devicePixelRatio).roundToInt() + val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo) + if (widthPx == 0 || heightPx == 0) return try { val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888) @@ -198,6 +209,5 @@ class HomeWidgetProvider : AppWidgetProvider() { StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(context, args) } StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(context, args) } } - } } 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 59920a934..4627eeb52 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt @@ -15,7 +15,6 @@ import deckers.thibault.aves.utils.ContextUtils.resourceUri import deckers.thibault.aves.utils.FlutterUtils import deckers.thibault.aves.utils.LogUtils import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.* import java.util.* @@ -74,14 +73,15 @@ class SearchSuggestionsProvider : ContentProvider() { } val messenger = flutterEngine!!.dartExecutor - val backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL) - backgroundChannel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result -> - when (call.method) { - "initialized" -> { - Log.d(LOG_TAG, "background channel is ready") - result.success(null) + val backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply { + setMethodCallHandler { call, result -> + when (call.method) { + "initialized" -> { + Log.d(LOG_TAG, "background channel is ready") + result.success(null) + } + else -> result.notImplemented() } - else -> result.notImplemented() } } 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 60c37d9ec..8192d1ce6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/WallpaperActivity.kt @@ -1,5 +1,6 @@ package deckers.thibault.aves +import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Build @@ -16,50 +17,22 @@ import deckers.thibault.aves.channel.streams.ImageByteStreamHandler import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.getParcelableExtraCompat import io.flutter.embedding.android.FlutterActivity +import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel class WallpaperActivity : FlutterActivity() { private lateinit var intentDataMap: MutableMap override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Log.i(LOG_TAG, "onCreate intent=$intent") intent.extras?.takeUnless { it.isEmpty }?.let { Log.i(LOG_TAG, "onCreate intent extras=$it") } - - super.onCreate(savedInstanceState) - - 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(context)) - 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 intentDataMap = extractIntentData(intent) - MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result -> - when (call.method) { - "getIntentData" -> { - result.success(intentDataMap) - intentDataMap.clear() - } - } - } + + initChannels(this) } override fun onStart() { @@ -76,6 +49,41 @@ 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" -> { + result.success(intentDataMap) + intentDataMap.clear() + } + } + } + private fun extractIntentData(intent: Intent?): MutableMap { when (intent?.action) { Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/AvesAppGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/AvesAppGlideModule.kt index 2b6f43d18..0b699c80a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/AvesAppGlideModule.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/AvesAppGlideModule.kt @@ -21,7 +21,7 @@ class AvesAppGlideModule : AppGlideModule() { override fun registerComponents(context: Context, glide: Glide, registry: Registry) { // prevent ExifInterface error logs // cf https://github.com/bumptech/glide/issues/3383 - glide.registry.imageHeaderParsers.compatRemoveIf { parser: ImageHeaderParser? -> parser is ExifInterfaceImageHeaderParser } + registry.imageHeaderParsers.compatRemoveIf { parser: ImageHeaderParser? -> parser is ExifInterfaceImageHeaderParser } } override fun isManifestParsingEnabled(): Boolean = false 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 540f37fa7..24cb4ec40 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 @@ -98,7 +98,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) - if (xmpBytes is ByteArray) { + if (xmpBytes is ByteArray && xmpBytes.size > 0) { val xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes, SafeXmpReader.PARSE_OPTIONS) processXmp(xmpMeta) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt index 06add40ae..8f6672ce9 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt @@ -67,6 +67,7 @@ object Helper { val metadata = when (fileType) { FileType.Jpeg -> safeReadJpeg(inputStream) + FileType.Png -> safeReadPng(inputStream) FileType.Tiff, FileType.Arw, FileType.Cr2, @@ -95,6 +96,10 @@ object Helper { return metadata } + private fun safeReadPng(input: InputStream): com.drew.metadata.Metadata { + return SafePngMetadataReader.readMetadata(input) + } + @Throws(IOException::class, TiffProcessingException::class) fun safeReadTiff(input: InputStream): com.drew.metadata.Metadata { val reader = RandomAccessStreamReader(input, RandomAccessStreamReader.DEFAULT_CHUNK_LENGTH, safeReadStreamLength) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt new file mode 100644 index 000000000..b0baa7973 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt @@ -0,0 +1,302 @@ +package deckers.thibault.aves.metadata.metadataextractor + +import android.util.Log +import com.drew.imaging.png.* +import com.drew.imaging.tiff.TiffProcessingException +import com.drew.imaging.tiff.TiffReader +import com.drew.lang.* +import com.drew.lang.annotations.NotNull +import com.drew.metadata.ErrorDirectory +import com.drew.metadata.Metadata +import com.drew.metadata.StringValue +import com.drew.metadata.exif.ExifTiffHandler +import com.drew.metadata.icc.IccReader +import com.drew.metadata.png.PngChromaticitiesDirectory +import com.drew.metadata.png.PngDirectory +import com.drew.metadata.xmp.XmpReader +import deckers.thibault.aves.utils.LogUtils +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import java.util.zip.InflaterInputStream +import java.util.zip.ZipException + +// adapted from `PngMetadataReader` to prevent reading OOM from large chunks +// as of `metadata-extractor` v2.18.0, there is no way to customize the reader +// without copying `desiredChunkTypes` and the whole `processChunk` function +object SafePngMetadataReader { + private val LOG_TAG = LogUtils.createTag() + + // arbitrary size to detect chunks that may yield an OOM + private const val chunkSizeDangerThreshold = SafeXmpReader.segmentTypeSizeDangerThreshold + + private val latin1Encoding = Charsets.ISO_8859_1 + private val desiredChunkTypes: Set = hashSetOf( + PngChunkType.IHDR, + PngChunkType.PLTE, + PngChunkType.tRNS, + PngChunkType.cHRM, + PngChunkType.sRGB, + PngChunkType.gAMA, + PngChunkType.iCCP, + PngChunkType.bKGD, + PngChunkType.tEXt, + PngChunkType.zTXt, + PngChunkType.iTXt, + PngChunkType.tIME, + PngChunkType.pHYs, + PngChunkType.sBIT, + PngChunkType.eXIf, + ) + + @Throws(IOException::class, PngProcessingException::class) + fun readMetadata(inputStream: InputStream): Metadata { + val chunks = PngChunkReader().extract(StreamReader(inputStream), desiredChunkTypes) + val metadata = Metadata() + for (chunk in chunks) { + try { + processChunk(metadata, chunk) + } catch (e: Exception) { + metadata.addDirectory(ErrorDirectory("Exception reading PNG chunk: " + e.message)) + } + } + return metadata + } + + @Throws(PngProcessingException::class, IOException::class) + private fun processChunk(@NotNull metadata: Metadata, @NotNull chunk: PngChunk) { + val chunkType = chunk.type + val bytes = chunk.bytes + + // TLAD insert start + if (bytes.size > chunkSizeDangerThreshold) { + Log.w(LOG_TAG, "PNG chunk $chunkType is too large, with a size of ${bytes.size} B") + return + } + // TLAD insert end + + if (chunkType == PngChunkType.IHDR) { + val header = PngHeader(bytes) + val directory = PngDirectory(PngChunkType.IHDR) + directory.setInt(PngDirectory.TAG_IMAGE_WIDTH, header.imageWidth) + directory.setInt(PngDirectory.TAG_IMAGE_HEIGHT, header.imageHeight) + directory.setInt(PngDirectory.TAG_BITS_PER_SAMPLE, header.bitsPerSample.toInt()) + directory.setInt(PngDirectory.TAG_COLOR_TYPE, header.colorType.numericValue) + directory.setInt(PngDirectory.TAG_COMPRESSION_TYPE, header.compressionType.toInt() and 0xFF) // make sure it's unsigned + directory.setInt(PngDirectory.TAG_FILTER_METHOD, header.filterMethod.toInt()) + directory.setInt(PngDirectory.TAG_INTERLACE_METHOD, header.interlaceMethod.toInt()) + metadata.addDirectory(directory) + } else if (chunkType == PngChunkType.PLTE) { + val directory = PngDirectory(PngChunkType.PLTE) + directory.setInt(PngDirectory.TAG_PALETTE_SIZE, bytes.size / 3) + metadata.addDirectory(directory) + } else if (chunkType == PngChunkType.tRNS) { + val directory = PngDirectory(PngChunkType.tRNS) + directory.setInt(PngDirectory.TAG_PALETTE_HAS_TRANSPARENCY, 1) + metadata.addDirectory(directory) + } else if (chunkType == PngChunkType.sRGB) { + val srgbRenderingIntent = bytes[0].toInt() + val directory = PngDirectory(PngChunkType.sRGB) + directory.setInt(PngDirectory.TAG_SRGB_RENDERING_INTENT, srgbRenderingIntent) + metadata.addDirectory(directory) + } else if (chunkType == PngChunkType.cHRM) { + val chromaticities = PngChromaticities(bytes) + val directory = PngChromaticitiesDirectory() + directory.setInt(PngChromaticitiesDirectory.TAG_WHITE_POINT_X, chromaticities.whitePointX) + directory.setInt(PngChromaticitiesDirectory.TAG_WHITE_POINT_Y, chromaticities.whitePointY) + directory.setInt(PngChromaticitiesDirectory.TAG_RED_X, chromaticities.redX) + directory.setInt(PngChromaticitiesDirectory.TAG_RED_Y, chromaticities.redY) + directory.setInt(PngChromaticitiesDirectory.TAG_GREEN_X, chromaticities.greenX) + directory.setInt(PngChromaticitiesDirectory.TAG_GREEN_Y, chromaticities.greenY) + directory.setInt(PngChromaticitiesDirectory.TAG_BLUE_X, chromaticities.blueX) + directory.setInt(PngChromaticitiesDirectory.TAG_BLUE_Y, chromaticities.blueY) + metadata.addDirectory(directory) + } else if (chunkType == PngChunkType.gAMA) { + val gammaInt = ByteConvert.toInt32BigEndian(bytes) + SequentialByteArrayReader(bytes).int32 + val directory = PngDirectory(PngChunkType.gAMA) + directory.setDouble(PngDirectory.TAG_GAMMA, gammaInt / 100000.0) + metadata.addDirectory(directory) + } else if (chunkType == PngChunkType.iCCP) { + val reader: SequentialReader = SequentialByteArrayReader(bytes) + + // Profile Name is 1-79 bytes, followed by the 1 byte null character + val profileNameBytes = reader.getNullTerminatedBytes(79 + 1) + val directory = PngDirectory(PngChunkType.iCCP) + directory.setStringValue(PngDirectory.TAG_ICC_PROFILE_NAME, StringValue(profileNameBytes, latin1Encoding)) + val compressionMethod = reader.int8 + // Only compression method allowed by the spec is zero: deflate + if (compressionMethod.toInt() == 0) { + // bytes left for compressed text is: + // total bytes length - (profilenamebytes length + null byte + compression method byte) + val bytesLeft = bytes.size - (profileNameBytes.size + 1 + 1) + val compressedProfile = reader.getBytes(bytesLeft) + try { + val inflateStream = InflaterInputStream(ByteArrayInputStream(compressedProfile)) + IccReader().extract(RandomAccessStreamReader(inflateStream), metadata, directory) + inflateStream.close() + } catch (zex: ZipException) { + directory.addError(String.format("Exception decompressing PNG iCCP chunk : %s", zex.message)) + metadata.addDirectory(directory) + } + } else { + directory.addError("Invalid compression method value") + } + metadata.addDirectory(directory) + } else if (chunkType == PngChunkType.bKGD) { + val directory = PngDirectory(PngChunkType.bKGD) + directory.setByteArray(PngDirectory.TAG_BACKGROUND_COLOR, bytes) + metadata.addDirectory(directory) + } else if (chunkType == PngChunkType.tEXt) { + val reader: SequentialReader = SequentialByteArrayReader(bytes) + + // Keyword is 1-79 bytes, followed by the 1 byte null character + val keywordsv = reader.getNullTerminatedStringValue(79 + 1, latin1Encoding) + val keyword = keywordsv.toString() + + // bytes left for text is: + // total bytes length - (Keyword length + null byte) + val bytesLeft = bytes.size - (keywordsv.bytes.size + 1) + val value = reader.getNullTerminatedStringValue(bytesLeft, latin1Encoding) + val textPairs: MutableList = ArrayList() + textPairs.add(KeyValuePair(keyword, value)) + val directory = PngDirectory(PngChunkType.tEXt) + directory.setObject(PngDirectory.TAG_TEXTUAL_DATA, textPairs) + metadata.addDirectory(directory) + } else if (chunkType == PngChunkType.zTXt) { + val reader: SequentialReader = SequentialByteArrayReader(bytes) + + // Keyword is 1-79 bytes, followed by the 1 byte null character + val keywordsv = reader.getNullTerminatedStringValue(79 + 1, latin1Encoding) + val keyword = keywordsv.toString() + val compressionMethod = reader.int8 + + // bytes left for compressed text is: + // total bytes length - (Keyword length + null byte + compression method byte) + val bytesLeft = bytes.size - (keywordsv.bytes.size + 1 + 1) + var textBytes: ByteArray? = null + if (compressionMethod.toInt() == 0) { + try { + textBytes = StreamUtil.readAllBytes(InflaterInputStream(ByteArrayInputStream(bytes, bytes.size - bytesLeft, bytesLeft))) + } catch (zex: ZipException) { + val directory = PngDirectory(PngChunkType.zTXt) + directory.addError(String.format("Exception decompressing PNG zTXt chunk with keyword \"%s\": %s", keyword, zex.message)) + metadata.addDirectory(directory) + } + } else { + val directory = PngDirectory(PngChunkType.zTXt) + directory.addError("Invalid compression method value") + metadata.addDirectory(directory) + } + if (textBytes != null) { + if (keyword == "XML:com.adobe.xmp") { + // NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary + XmpReader().extract(textBytes, metadata) + } else { + val textPairs: MutableList = ArrayList() + textPairs.add(KeyValuePair(keyword, StringValue(textBytes, latin1Encoding))) + val directory = PngDirectory(PngChunkType.zTXt) + directory.setObject(PngDirectory.TAG_TEXTUAL_DATA, textPairs) + metadata.addDirectory(directory) + } + } + } else if (chunkType == PngChunkType.iTXt) { + val reader: SequentialReader = SequentialByteArrayReader(bytes) + + // Keyword is 1-79 bytes, followed by the 1 byte null character + val keywordsv = reader.getNullTerminatedStringValue(79 + 1, latin1Encoding) + val keyword = keywordsv.toString() + val compressionFlag = reader.int8 + val compressionMethod = reader.int8 + // TODO we currently ignore languageTagBytes and translatedKeywordBytes + val languageTagBytes = reader.getNullTerminatedBytes(bytes.size) + val translatedKeywordBytes = reader.getNullTerminatedBytes(bytes.size) + + // bytes left for compressed text is: + // total bytes length - (Keyword length + null byte + comp flag byte + comp method byte + lang length + null byte + translated length + null byte) + val bytesLeft = bytes.size - (keywordsv.bytes.size + 1 + 1 + 1 + languageTagBytes.size + 1 + translatedKeywordBytes.size + 1) + var textBytes: ByteArray? = null + if (compressionFlag.toInt() == 0) { + textBytes = reader.getNullTerminatedBytes(bytesLeft) + } else if (compressionFlag.toInt() == 1) { + if (compressionMethod.toInt() == 0) { + try { + textBytes = StreamUtil.readAllBytes(InflaterInputStream(ByteArrayInputStream(bytes, bytes.size - bytesLeft, bytesLeft))) + } catch (zex: ZipException) { + val directory = PngDirectory(PngChunkType.iTXt) + directory.addError(String.format("Exception decompressing PNG iTXt chunk with keyword \"%s\": %s", keyword, zex.message)) + metadata.addDirectory(directory) + } + } else { + val directory = PngDirectory(PngChunkType.iTXt) + directory.addError("Invalid compression method value") + metadata.addDirectory(directory) + } + } else { + val directory = PngDirectory(PngChunkType.iTXt) + directory.addError("Invalid compression flag value") + metadata.addDirectory(directory) + } + if (textBytes != null) { + if (keyword == "XML:com.adobe.xmp") { + // NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary + XmpReader().extract(textBytes, metadata) + } else { + val textPairs: MutableList = ArrayList() + textPairs.add(KeyValuePair(keyword, StringValue(textBytes, latin1Encoding))) + val directory = PngDirectory(PngChunkType.iTXt) + directory.setObject(PngDirectory.TAG_TEXTUAL_DATA, textPairs) + metadata.addDirectory(directory) + } + } + } else if (chunkType == PngChunkType.tIME) { + val reader = SequentialByteArrayReader(bytes) + val year = reader.uInt16 + val month = reader.uInt8.toInt() + val day = reader.uInt8.toInt() + val hour = reader.uInt8.toInt() + val minute = reader.uInt8.toInt() + val second = reader.uInt8.toInt() + val directory = PngDirectory(PngChunkType.tIME) + if (DateUtil.isValidDate(year, month - 1, day) && DateUtil.isValidTime(hour, minute, second)) { + val dateString = String.format("%04d:%02d:%02d %02d:%02d:%02d", year, month, day, hour, minute, second) + directory.setString(PngDirectory.TAG_LAST_MODIFICATION_TIME, dateString) + } else { + directory.addError( + String.format( + "PNG tIME data describes an invalid date/time: year=%d month=%d day=%d hour=%d minute=%d second=%d", + year, month, day, hour, minute, second + ) + ) + } + metadata.addDirectory(directory) + } else if (chunkType == PngChunkType.pHYs) { + val reader = SequentialByteArrayReader(bytes) + val pixelsPerUnitX = reader.int32 + val pixelsPerUnitY = reader.int32 + val unitSpecifier = reader.int8 + val directory = PngDirectory(PngChunkType.pHYs) + directory.setInt(PngDirectory.TAG_PIXELS_PER_UNIT_X, pixelsPerUnitX) + directory.setInt(PngDirectory.TAG_PIXELS_PER_UNIT_Y, pixelsPerUnitY) + directory.setInt(PngDirectory.TAG_UNIT_SPECIFIER, unitSpecifier.toInt()) + metadata.addDirectory(directory) + } else if (chunkType == PngChunkType.sBIT) { + val directory = PngDirectory(PngChunkType.sBIT) + directory.setByteArray(PngDirectory.TAG_SIGNIFICANT_BITS, bytes) + metadata.addDirectory(directory) + } else if (chunkType == PngChunkType.eXIf) { + try { + val handler = ExifTiffHandler(metadata, null) + TiffReader().processTiff(ByteArrayReader(bytes), handler, 0) + } catch (ex: TiffProcessingException) { + val directory = PngDirectory(PngChunkType.eXIf) + directory.addError(ex.message) + metadata.addDirectory(directory) + } catch (ex: IOException) { + val directory = PngDirectory(PngChunkType.eXIf) + directory.addError(ex.message) + metadata.addDirectory(directory) + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt index c70c09ae2..db58da6d6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt @@ -135,7 +135,7 @@ class SafeXmpReader : XmpReader() { private val LOG_TAG = LogUtils.createTag() // arbitrary size to detect extended XMP that may yield an OOM - private const val segmentTypeSizeDangerThreshold = 3 * (1 shl 20) // MB + const val segmentTypeSizeDangerThreshold = 3 * (1 shl 20) // MB // tighter node limits for faster loading val PARSE_OPTIONS: ParseOptions = ParseOptions().setXMPNodesToLimit( 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 4e7ff6ca3..9d5749561 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 @@ -41,6 +41,7 @@ class SourceEntry { var height: Int? = null private var sourceRotationDegrees: Int? = null private var sizeBytes: Long? = null + private var dateAddedSecs: Long? = null private var dateModifiedSecs: Long? = null private var sourceDateTakenMillis: Long? = null private var durationMillis: Long? = null @@ -61,6 +62,7 @@ class SourceEntry { sourceRotationDegrees = map["sourceRotationDegrees"] as Int? sizeBytes = toLong(map["sizeBytes"]) title = map["title"] as String? + dateAddedSecs = toLong(map["dateAddedSecs"]) dateModifiedSecs = toLong(map["dateModifiedSecs"]) sourceDateTakenMillis = toLong(map["sourceDateTakenMillis"]) durationMillis = toLong(map["durationMillis"]) @@ -83,6 +85,7 @@ class SourceEntry { "sourceRotationDegrees" to (sourceRotationDegrees ?: 0), "sizeBytes" to sizeBytes, "title" to title, + "dateAddedSecs" to dateAddedSecs, "dateModifiedSecs" to dateModifiedSecs, "sourceDateTakenMillis" to sourceDateTakenMillis, "durationMillis" to durationMillis, 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 66d6f1035..3c3a624d5 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 @@ -22,7 +22,12 @@ internal class FileImageProvider : ImageProvider() { try { val file = File(path) if (file.exists()) { - entry.initFromFile(path, file.name, file.length(), file.lastModified() / 1000) + entry.initFromFile( + path = path, + title = file.name, + sizeBytes = file.length(), + dateModifiedSecs = file.lastModified() / 1000, + ) } } catch (e: SecurityException) { callback.onFailure(e) 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 442d35190..d13143269 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 @@ -7,7 +7,6 @@ import android.content.* import android.media.MediaScannerConnection import android.net.Uri import android.os.Build -import android.os.Environment import android.provider.MediaStore import android.util.Log import com.commonsware.cwac.document.DocumentFileCompat @@ -87,7 +86,7 @@ class MediaStoreImageProvider : ImageProvider() { val alwaysValid: NewEntryChecker = fun(_: Int, _: Int): Boolean = true val onSuccess: NewEntryHandler = fun(entry: FieldMap) { fetched.add(entry) } if (id != null) { - if (!found && (sourceMimeType == null || isImage(sourceMimeType))) { + if (sourceMimeType == null || isImage(sourceMimeType)) { val contentUri = ContentUris.withAppendedId(IMAGE_CONTENT_URI, id) found = fetchFrom(context, alwaysValid, onSuccess, contentUri, IMAGE_PROJECTION) } @@ -190,6 +189,7 @@ class MediaStoreImageProvider : ImageProvider() { val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE) val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH) val heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT) + val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED) val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) val dateTakenColumn = cursor.getColumnIndex(MediaColumns.DATE_TAKEN) @@ -225,6 +225,7 @@ class MediaStoreImageProvider : ImageProvider() { "height" to height, "sourceRotationDegrees" to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0, "sizeBytes" to cursor.getLong(sizeColumn), + "dateAddedSecs" to cursor.getInt(dateAddedColumn), "dateModifiedSecs" to dateModifiedSecs, "sourceDateTakenMillis" to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null, "durationMillis" to durationMillis, @@ -391,8 +392,13 @@ class MediaStoreImageProvider : ImageProvider() { effectiveTargetDir = targetDir targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir) if (!File(targetDir).exists()) { - callback.onFailure(Exception("failed to create directory at path=$targetDir")) - return + val downloadDirPath = StorageUtils.getDownloadDirPath(activity, targetDir) + val isDownloadSubdir = downloadDirPath != null && targetDir.startsWith(downloadDirPath) + // download subdirectories can be created later by Media Store insertion + if (!isDownloadSubdir) { + callback.onFailure(Exception("failed to create directory at path=$targetDir")) + return + } } } @@ -535,54 +541,57 @@ class MediaStoreImageProvider : ImageProvider() { targetNameWithoutExtension: String, write: (OutputStream) -> Unit, ): String { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && isDownloadDir(activity, targetDir)) { - val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}" - val values = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, targetFileName) - put(MediaStore.MediaColumns.IS_PENDING, 1) - } - val resolver = activity.contentResolver - val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val downloadDirPath = StorageUtils.getDownloadDirPath(activity, targetDir) + val isDownloadSubdir = downloadDirPath != null && targetDir.startsWith(downloadDirPath) + if (isDownloadSubdir) { + val volumePath = StorageUtils.getVolumePath(activity, targetDir) + val relativePath = targetDir.substring(volumePath?.length ?: 0) - uri?.let { - resolver.openOutputStream(uri)?.use(write) - values.clear() - values.put(MediaStore.MediaColumns.IS_PENDING, 0) - resolver.update(uri, values, null, null) - } ?: throw Exception("MediaStore failed for some reason") - - File(targetDir, targetFileName).path - } else { - targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir") - - // the file created from a `TreeDocumentFile` is also a `TreeDocumentFile` - // but in order to open an output stream to it, we need to use a `SingleDocumentFile` - // through a document URI, not a tree URI - // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first - val targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension) - val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri) - - try { - targetDocFile.openOutputStream().use(write) - } catch (e: Exception) { - // remove empty file - if (targetDocFile.exists()) { - targetDocFile.delete() + val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}" + val values = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, targetFileName) + put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) + put(MediaStore.MediaColumns.IS_PENDING, 1) } - throw e + val resolver = activity.contentResolver + val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) + + uri?.let { + resolver.openOutputStream(uri)?.use(write) + values.clear() + values.put(MediaStore.MediaColumns.IS_PENDING, 0) + resolver.update(uri, values, null, null) + } ?: throw Exception("MediaStore failed for some reason") + + return File(targetDir, targetFileName).path } - - // the source file name and the created document file name can be different when: - // - a file with the same name already exists, some implementations give a suffix like ` (1)`, some *do not* - // - the original extension does not match the extension added by the underlying provider - val fileName = targetDocFile.name - targetDir + fileName } - } - private fun isDownloadDir(context: Context, dirPath: String): Boolean { - val relativeDir = removeTrailingSeparator(PathSegments(context, dirPath).relativeDir ?: "") - return relativeDir == Environment.DIRECTORY_DOWNLOADS + targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir") + + // the file created from a `TreeDocumentFile` is also a `TreeDocumentFile` + // but in order to open an output stream to it, we need to use a `SingleDocumentFile` + // through a document URI, not a tree URI + // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first + val targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension) + val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri) + + try { + targetDocFile.openOutputStream().use(write) + } catch (e: Exception) { + // remove empty file + if (targetDocFile.exists()) { + targetDocFile.delete() + } + throw e + } + + // the source file name and the created document file name can be different when: + // - a file with the same name already exists, some implementations give a suffix like ` (1)`, some *do not* + // - the original extension does not match the extension added by the underlying provider + val fileName = targetDocFile.name + return targetDir + fileName } override suspend fun renameMultiple( @@ -782,6 +791,7 @@ class MediaStoreImageProvider : ImageProvider() { // we retrieve updated fields as the renamed/moved file became a new entry in the Media Store val projection = arrayOf( + MediaStore.MediaColumns.DATE_ADDED, MediaStore.MediaColumns.DATE_MODIFIED, ) try { @@ -791,6 +801,7 @@ class MediaStoreImageProvider : ImageProvider() { newFields["uri"] = uri.toString() newFields["contentId"] = uri.tryParseId() newFields["path"] = path + cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED).let { if (it != -1) newFields["dateAddedSecs"] = cursor.getInt(it) } cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) } cursor.close() return newFields @@ -864,6 +875,7 @@ class MediaStoreImageProvider : ImageProvider() { MediaStore.MediaColumns.SIZE, MediaStore.MediaColumns.WIDTH, MediaStore.MediaColumns.HEIGHT, + MediaStore.MediaColumns.DATE_ADDED, MediaStore.MediaColumns.DATE_MODIFIED, MediaColumns.DATE_TAKEN, ) 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 55a134192..6f7cf280e 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 @@ -104,9 +104,8 @@ object MimeTypes { else -> false } - // as of androidx.exifinterface:exifinterface:1.3.3 + // as of androidx.exifinterface:exifinterface:1.3.4 fun canEditExif(mimeType: String) = when (mimeType) { - DNG, JPEG, PNG, WEBP -> true 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 2d78f9a52..08355a5de 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 @@ -105,7 +105,11 @@ object PermissionManager { val primaryDir = dirSegments.firstOrNull() if (getRestrictedPrimaryDirectories().contains(primaryDir) && dirSegments.size > 1) { // request secondary directory (if any) for restricted primary directory - dirSet.add(dirSegments.take(2).joinToString(File.separator)) + val dir = dirSegments.take(2).joinToString(File.separator) + // only register directories that exist on storage, so they can be selected for access grant + if (File(volumePath, dir).exists()) { + dirSet.add(dir) + } } else { primaryDir?.let { dirSet.add(it) } } 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 fef6748b5..db1bd8e80 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 @@ -9,6 +9,7 @@ import android.content.pm.PackageManager import android.media.MediaMetadataRetriever import android.net.Uri import android.os.Build +import android.os.Environment import android.os.storage.StorageManager import android.provider.DocumentsContract import android.provider.MediaStore @@ -93,6 +94,10 @@ object StorageUtils { return getVolumePaths(context).firstOrNull { anyPath.startsWith(it) } } + fun getDownloadDirPath(context: Context, anyPath: String): String? { + return getVolumePath(context, anyPath)?.let { volumePath -> ensureTrailingSeparator(File(volumePath, Environment.DIRECTORY_DOWNLOADS).path) } + } + private fun getPathStepIterator(context: Context, anyPath: String, root: String?): Iterator? { val rootLength = (root ?: getVolumePath(context, anyPath))?.length ?: return null diff --git a/android/build.gradle b/android/build.gradle index f35c09e44..42c5dc7f7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,13 +7,13 @@ buildscript { maven { url 'https://developer.huawei.com/repo/' } } dependencies { - classpath 'com.android.tools.build:gradle:7.2.2' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // GMS & Firebase Crashlytics (used by some flavors only) classpath 'com.google.gms:google-services:4.3.14' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2' // HMS (used by some flavors only) - classpath 'com.huawei.agconnect:agcp:1.5.2.300' + classpath 'com.huawei.agconnect:agcp:1.7.2.300' } } @@ -21,7 +21,7 @@ allprojects { repositories { google() mavenCentral() - maven {url 'https://developer.huawei.com/repo/'} + maven { url 'https://developer.huawei.com/repo/' } } // gradle.projectsEvaluated { // tasks.withType(JavaCompile) { diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 080650cd5..e0e447a3d 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.3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/fastlane/metadata/android/de/images/phoneScreenshots/1.png b/fastlane/metadata/android/de/images/phoneScreenshots/1.png index ad9cb124a..ff49b0a6e 100644 Binary files a/fastlane/metadata/android/de/images/phoneScreenshots/1.png and b/fastlane/metadata/android/de/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/el/images/phoneScreenshots/1.png b/fastlane/metadata/android/el/images/phoneScreenshots/1.png index 54c24fa81..eefd8f1bb 100644 Binary files a/fastlane/metadata/android/el/images/phoneScreenshots/1.png and b/fastlane/metadata/android/el/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/changelogs/1081.txt b/fastlane/metadata/android/en-US/changelogs/1081.txt new file mode 100644 index 000000000..e5b6ac839 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/1081.txt @@ -0,0 +1,5 @@ +In v1.7.1: +- view your photos with the mosaic layout +- reverse filters to filter out/in +- set wallpapers with scroll effect +Full changelog available on GitHub \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index eac80ee54..da2db556d 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/es-MX/images/phoneScreenshots/1.png b/fastlane/metadata/android/es-MX/images/phoneScreenshots/1.png index 2615a94a2..7c0e52710 100644 Binary files a/fastlane/metadata/android/es-MX/images/phoneScreenshots/1.png and b/fastlane/metadata/android/es-MX/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/fr/images/phoneScreenshots/1.png b/fastlane/metadata/android/fr/images/phoneScreenshots/1.png index 985209ece..40ad40622 100644 Binary files a/fastlane/metadata/android/fr/images/phoneScreenshots/1.png and b/fastlane/metadata/android/fr/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/id/images/phoneScreenshots/1.png b/fastlane/metadata/android/id/images/phoneScreenshots/1.png index 7b5bb8c8d..fc94cf155 100644 Binary files a/fastlane/metadata/android/id/images/phoneScreenshots/1.png and b/fastlane/metadata/android/id/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/it/images/phoneScreenshots/1.png b/fastlane/metadata/android/it/images/phoneScreenshots/1.png index 9489221ca..d6b796478 100644 Binary files a/fastlane/metadata/android/it/images/phoneScreenshots/1.png and b/fastlane/metadata/android/it/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/ja/images/phoneScreenshots/1.png b/fastlane/metadata/android/ja/images/phoneScreenshots/1.png index 7113ce3c1..197d4d7c9 100644 Binary files a/fastlane/metadata/android/ja/images/phoneScreenshots/1.png and b/fastlane/metadata/android/ja/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/ko/images/phoneScreenshots/1.png b/fastlane/metadata/android/ko/images/phoneScreenshots/1.png index af02b0701..dfafa5766 100644 Binary files a/fastlane/metadata/android/ko/images/phoneScreenshots/1.png and b/fastlane/metadata/android/ko/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/nl/images/phoneScreenshots/1.png b/fastlane/metadata/android/nl/images/phoneScreenshots/1.png index d10712612..5a9beeef1 100644 Binary files a/fastlane/metadata/android/nl/images/phoneScreenshots/1.png and b/fastlane/metadata/android/nl/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/1.png b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/1.png index 44b10be73..e09364a89 100644 Binary files a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/1.png and b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/ru/images/phoneScreenshots/1.png b/fastlane/metadata/android/ru/images/phoneScreenshots/1.png index a9aa6c997..59abfcced 100644 Binary files a/fastlane/metadata/android/ru/images/phoneScreenshots/1.png and b/fastlane/metadata/android/ru/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/tr/images/phoneScreenshots/1.png b/fastlane/metadata/android/tr/images/phoneScreenshots/1.png index b1fe36389..dd9987e27 100644 Binary files a/fastlane/metadata/android/tr/images/phoneScreenshots/1.png and b/fastlane/metadata/android/tr/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/zh-CN/images/phoneScreenshots/1.png b/fastlane/metadata/android/zh-CN/images/phoneScreenshots/1.png index 7b493db17..d9e7b1573 100644 Binary files a/fastlane/metadata/android/zh-CN/images/phoneScreenshots/1.png and b/fastlane/metadata/android/zh-CN/images/phoneScreenshots/1.png differ diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 595392152..4d7abfcab 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -41,6 +41,8 @@ "chipActionGoToAlbumPage": "Anzeigen in Alben", "chipActionGoToCountryPage": "Anzeigen in Ländern", "chipActionGoToTagPage": "Zeige in Tags", + "chipActionFilterOut": "Filtern ohne", + "chipActionFilterIn": "Filtern mit", "chipActionHide": "Ausblenden", "chipActionPin": "Oben Anpinnen", "chipActionUnpin": "Nicht mehr Anpinen", @@ -88,15 +90,18 @@ "entryInfoActionEditDate": "Datum & Uhrzeit bearbeiten", "entryInfoActionEditLocation": "Standort bearbeiten", + "entryInfoActionEditTitleDescription": "Titel und Beschreibung bearbeiten", "entryInfoActionEditRating": "Bewertung bearbeiten", "entryInfoActionEditTags": "Tags bearbeiten", "entryInfoActionRemoveMetadata": "Metadaten entfernen", "filterBinLabel": "Papierkorb", "filterFavouriteLabel": "Favorit", + "filterNoDateLabel": "Undatiert", "filterNoLocationLabel": "Ungeortet", "filterNoRatingLabel": "Nicht bewertet", "filterNoTagLabel": "Unmarkiert", + "filterNoTitleLabel": "Unbenannt", "filterOnThisDayLabel": "Am heutigen Tag", "filterRecentlyAddedLabel": "Kürzlich hinzugefügt", "filterRatingRejectedLabel": "Verworfen", @@ -152,9 +157,9 @@ "displayRefreshRatePreferHighest": "Höchste Rate", "displayRefreshRatePreferLowest": "Niedrigste Rate", - "slideshowVideoPlaybackSkip": "Überspringen", - "slideshowVideoPlaybackMuted": "Stumm abspielen", - "slideshowVideoPlaybackWithSound": "Mit Ton abspielen", + "videoPlaybackSkip": "Überspringen", + "videoPlaybackMuted": "Stumm abspielen", + "videoPlaybackWithSound": "Mit Ton abspielen", "themeBrightnessLight": "Hell", "themeBrightnessDark": "Dunkel", @@ -164,11 +169,15 @@ "viewerTransitionParallax": "Parallaxe", "viewerTransitionFade": "Ausblenden", "viewerTransitionZoomIn": "Heranzoomen", + "viewerTransitionNone": "Keine", "wallpaperTargetHome": "Startbildschirm", "wallpaperTargetLock": "Sperrbildschirm", "wallpaperTargetHomeLock": "Start- und Sperrbildschirm", + "widgetOpenPageHome": "Startseite öffnen", + "widgetOpenPageViewer": "Viewer öffnen", + "albumTierNew": "Neu", "albumTierPinned": "Angeheftet", "albumTierSpecial": "Häufig verwendet", @@ -284,7 +293,9 @@ "viewDialogSortSectionTitle": "Sortieren", "viewDialogGroupSectionTitle": "Gruppe", "viewDialogLayoutSectionTitle": "Layout", + "viewDialogReverseSortOrder": "Umgekehrte Sortierung", + "tileLayoutMosaic": "Mosaik", "tileLayoutGrid": "Kacheln", "tileLayoutList": "Liste", @@ -387,10 +398,22 @@ "sortByAlbumFileName": "Nach Album & Dateiname", "sortByRating": "Nach Bewertung", + "sortOrderNewestFirst": "Neueste zuerst", + "sortOrderOldestFirst": "Älteste zuerst", + "sortOrderAtoZ": "A zu Z", + "sortOrderZtoA": "Z zu A", + "sortOrderHighestFirst": "Höchste zuerst", + "sortOrderLowestFirst": "Niedrigste zuerst", + "sortOrderLargestFirst": "Größtes zuerst", + "sortOrderSmallestFirst": "Kleinste zuerst", + "albumGroupTier": "Nach Ebene", + "albumGroupType": "Nach Typ", "albumGroupVolume": "Nach Speichervolumen", "albumGroupNone": "Nicht gruppieren", + "albumMimeTypeMixed": "Gemischt", + "albumPickPageTitleCopy": "In Album kopieren", "albumPickPageTitleExport": "In Album exportieren", "albumPickPageTitleMove": "Zum Album verschieben", @@ -424,10 +447,12 @@ "searchPlacesSectionTitle": "Orte", "searchTagsSectionTitle": "Tags", "searchRatingSectionTitle": "Bewertungen", + "searchMetadataSectionTitle": "Metadaten", "settingsPageTitle": "Einstellungen", "settingsSystemDefault": "System", "settingsDefault": "Standard", + "settingsDisabled": "Deaktiviert", "settingsSearchFieldLabel": "Einstellungen durchsuchen", "settingsSearchEmpty": "Keine passende Einstellung", @@ -510,10 +535,9 @@ "settingsSlideshowRepeat": "Wiederholung", "settingsSlideshowShuffle": "Mischen", "settingsSlideshowFillScreen": "Bildschirm ausfüllen", + "settingsSlideshowAnimatedZoomEffect": "Animierter Zoomeffekt", "settingsSlideshowTransitionTile": "Übergang", - "settingsSlideshowTransitionDialogTitle": "Übergang", "settingsSlideshowIntervalTile": "Intervall", - "settingsSlideshowIntervalDialogTitle": "Intervall", "settingsSlideshowVideoPlaybackTile": "Videowiedergabe", "settingsSlideshowVideoPlaybackDialogTitle": "Videowiedergabe", @@ -521,7 +545,7 @@ "settingsVideoSectionTitle": "Video", "settingsVideoShowVideos": "Videos anzeigen", "settingsVideoEnableHardwareAcceleration": "Hardware-Beschleunigung", - "settingsVideoEnableAutoPlay": "Automatische Wiedergabe", + "settingsVideoAutoPlay": "Automatische Wiedergabe", "settingsVideoLoopModeTile": "Schleifen-Modus", "settingsVideoLoopModeDialogTitle": "Schleifen-Modus", @@ -543,7 +567,6 @@ "settingsVideoControlsTile": "Steuerung", "settingsVideoControlsPageTitle": "Steuerung", "settingsVideoButtonsTile": "Schaltflächen", - "settingsVideoButtonsDialogTitle": "Schaltflächen", "settingsVideoGestureDoubleTapTogglePlay": "Doppeltippen zum Abspielen/Pausieren", "settingsVideoGestureSideDoubleTapSeek": "Doppeltippen auf die Bildschirmränder zum Rückwärts-/Vorwärtsspringen", @@ -576,7 +599,6 @@ "settingsRemoveAnimationsTile": "Animationen entfernen", "settingsRemoveAnimationsDialogTitle": "Animationen entfernen", "settingsTimeToTakeActionTile": "Zeit zum Reagieren", - "settingsTimeToTakeActionDialogTitle": "Zeit zum Reagieren", "settingsDisplaySectionTitle": "Anzeige", "settingsThemeBrightnessTile": "Thema", @@ -598,6 +620,7 @@ "settingsWidgetPageTitle": "Bilderrahmen", "settingsWidgetShowOutline": "Gliederung", + "settingsWidgetOpenPage": "Beim Tippen auf das Widget", "settingsCollectionTile": "Sammlung", @@ -606,6 +629,7 @@ "statsTopCountriesSectionTitle": "Top-Länder", "statsTopPlacesSectionTitle": "Top-Plätze", "statsTopTagsSectionTitle": "Top-Tags", + "statsTopAlbumsSectionTitle": "Top-Alben", "viewerOpenPanoramaButtonLabel": "ÖFFNE PANORAMA", "viewerSetWallpaperButtonLabel": "HINTERGRUNDBILD EINSTELLEN", @@ -650,6 +674,8 @@ "viewerInfoSearchSuggestionResolution": "Auflösung", "viewerInfoSearchSuggestionRights": "Rechte", + "wallpaperUseScrollEffect": "Scroll-Effekt auf dem Startbildschirm verwenden", + "tagEditorPageTitle": "Tags bearbeiten", "tagEditorPageNewTagFieldLabel": "Neuer Tag", "tagEditorPageAddTagTooltip": "Tag hinzufügen", diff --git a/lib/l10n/app_el.arb b/lib/l10n/app_el.arb index f00605985..a9d77008e 100644 --- a/lib/l10n/app_el.arb +++ b/lib/l10n/app_el.arb @@ -41,6 +41,8 @@ "chipActionGoToAlbumPage": "Εμφάνιση στα Άλμπουμ", "chipActionGoToCountryPage": "Εμφάνιση στις χώρες", "chipActionGoToTagPage": "Εμφάνιση στις ετικέτες", + "chipActionFilterOut": "Χωρίς φιλτράρισμα", + "chipActionFilterIn": "Με φιλτράρισμα", "chipActionHide": "Απόκρυψη", "chipActionPin": "Καρφίτσωμα στην κορυφή", "chipActionUnpin": "Ξέκαρφίτσωμα από την κορυφή", @@ -155,9 +157,9 @@ "displayRefreshRatePreferHighest": "Υψηλότερος ρυθμός", "displayRefreshRatePreferLowest": "Χαμηλότερος ρυθμός", - "slideshowVideoPlaybackSkip": "Παράλειψη", - "slideshowVideoPlaybackMuted": "Αναπαραγωγή σε σίγαση", - "slideshowVideoPlaybackWithSound": "Αναπαραγωγή με ήχο", + "videoPlaybackSkip": "Παράλειψη", + "videoPlaybackMuted": "Αναπαραγωγή σε σίγαση", + "videoPlaybackWithSound": "Αναπαραγωγή με ήχο", "themeBrightnessLight": "Φωτεινό", "themeBrightnessDark": "Σκούρο", @@ -167,11 +169,15 @@ "viewerTransitionParallax": "Παράλλαξη", "viewerTransitionFade": "Ξεθώριασμα", "viewerTransitionZoomIn": "Μεγέθυνση", + "viewerTransitionNone": "Καμία", "wallpaperTargetHome": "Αρχική οθόνη", "wallpaperTargetLock": "Οθόνη κλειδώματος", "wallpaperTargetHomeLock": "Αρχική οθόνη και οθόνη κλειδώματος", + "widgetOpenPageHome": "Άνοιγμα αρχικής σελίδας", + "widgetOpenPageViewer": "Άνοιγμα προβολέα αρχείων", + "albumTierNew": "Νέα", "albumTierPinned": "Καρφιτσωμένα", "albumTierSpecial": "Συστήματος", @@ -289,6 +295,7 @@ "viewDialogLayoutSectionTitle": "Διαταξη", "viewDialogReverseSortOrder": "Αντίστροφη σειρά ταξινόμησης", + "tileLayoutMosaic": "Ψηφιδωτό", "tileLayoutGrid": "Πλέγμα", "tileLayoutList": "Λίστα", @@ -401,9 +408,12 @@ "sortOrderSmallestFirst": "Τα μικρότερα πρώτα", "albumGroupTier": "Ανά βαθμίδα", + "albumGroupType": "Ανά τύπο", "albumGroupVolume": "Ανά αποθηκευτική μονάδα", "albumGroupNone": "Να μην γίνει ομαδοποίηση", + "albumMimeTypeMixed": "Μικτα", + "albumPickPageTitleCopy": "Αντιγραφή στο άλμπουμ", "albumPickPageTitleExport": "Εξαγωγή στο άλμπουμ", "albumPickPageTitleMove": "Μετακίνηση στο άλμπουμ", @@ -442,6 +452,7 @@ "settingsPageTitle": "Ρυθμισεις", "settingsSystemDefault": "Σύστημα", "settingsDefault": "Προεπιλογή", + "settingsDisabled": "Απενεργοποιημένο", "settingsSearchFieldLabel": "Αναζήτηση ρυθμίσεων", "settingsSearchEmpty": "Δεν υπάρχει αντίστοιχη ρύθμιση", @@ -524,10 +535,9 @@ "settingsSlideshowRepeat": "Επανάληψη", "settingsSlideshowShuffle": "Τυχαία σειρά", "settingsSlideshowFillScreen": "Χρησιμοποίηση πλήρης οθόνης", + "settingsSlideshowAnimatedZoomEffect": "Εφέ κινούμενου ζουμ", "settingsSlideshowTransitionTile": "Μετάβαση", - "settingsSlideshowTransitionDialogTitle": "Μεταβαση", "settingsSlideshowIntervalTile": "Διάρκεια", - "settingsSlideshowIntervalDialogTitle": "Διαρκεια", "settingsSlideshowVideoPlaybackTile": "Αναπαραγωγή βίντεο", "settingsSlideshowVideoPlaybackDialogTitle": "Αναπαραγωγη Βιντεο", @@ -535,7 +545,7 @@ "settingsVideoSectionTitle": "Βιντεο", "settingsVideoShowVideos": "Εμφάνιση των βίντεο στη συλλογή", "settingsVideoEnableHardwareAcceleration": "Επιτάχυνση υλισμικού", - "settingsVideoEnableAutoPlay": "Αυτόματη αναπαραγωγή κατά το άνοιγμα", + "settingsVideoAutoPlay": "Αυτόματη αναπαραγωγή κατά το άνοιγμα", "settingsVideoLoopModeTile": "Επανάληψη αυτόματα στο τέλος κάθε βίντεο", "settingsVideoLoopModeDialogTitle": "Επαναληψη Αυτοματα στο Τελος Καθε Βιντεο", @@ -557,7 +567,6 @@ "settingsVideoControlsTile": "Έλεγχος", "settingsVideoControlsPageTitle": "Ελεγχος", "settingsVideoButtonsTile": "Κουμπιά", - "settingsVideoButtonsDialogTitle": "Κουμπια", "settingsVideoGestureDoubleTapTogglePlay": "Αγγίξτε την οθόνη δύο φορές για αναπαραγωγή/παύση", "settingsVideoGestureSideDoubleTapSeek": "Αγγίξτε δύο φορές τις άκρες της οθόνης για να πάτε πίσω/εμπρός", @@ -590,7 +599,6 @@ "settingsRemoveAnimationsTile": "Κατάργηση κινούμενων εικόνων", "settingsRemoveAnimationsDialogTitle": "Καταργηση Κινουμενων Εικονων", "settingsTimeToTakeActionTile": "Χρόνος λήψης ενεργειών", - "settingsTimeToTakeActionDialogTitle": "Χρονος Ληψης Ενεργειων", "settingsDisplaySectionTitle": "Οθονη", "settingsThemeBrightnessTile": "Θέμα", @@ -612,6 +620,7 @@ "settingsWidgetPageTitle": "Κορνιζα", "settingsWidgetShowOutline": "Περίγραμμα", + "settingsWidgetOpenPage": "Όταν πατάτε στο γραφικό στοιχείο", "settingsCollectionTile": "Συλλογή", @@ -620,6 +629,7 @@ "statsTopCountriesSectionTitle": "Κορυφαιες Χωρες", "statsTopPlacesSectionTitle": "Κορυφαια Μερη", "statsTopTagsSectionTitle": "Κορυφαιες Ετικετες", + "statsTopAlbumsSectionTitle": "Κορυφαια Αλμπουμ", "viewerOpenPanoramaButtonLabel": "Άνοιγμα πανοραμικών", "viewerSetWallpaperButtonLabel": "ΟΡΙΣΜΟΣ ΤΑΠΕΤΣΑΡΙΑΣ", @@ -664,6 +674,8 @@ "viewerInfoSearchSuggestionResolution": "Ανάλυση", "viewerInfoSearchSuggestionRights": "Δικαιώματα", + "wallpaperUseScrollEffect": "Εφέ κύλισης στην αρχική οθόνη", + "tagEditorPageTitle": "Επεξεργασια Ετικετων", "tagEditorPageNewTagFieldLabel": "Νέα ετικέτα", "tagEditorPageAddTagTooltip": "Προσθήκη ετικέτας", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b563fe5f1..13901991d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -69,6 +69,8 @@ "chipActionGoToAlbumPage": "Show in Albums", "chipActionGoToCountryPage": "Show in Countries", "chipActionGoToTagPage": "Show in Tags", + "chipActionFilterOut": "Filter out", + "chipActionFilterIn": "Filter in", "chipActionHide": "Hide", "chipActionPin": "Pin to top", "chipActionUnpin": "Unpin from top", @@ -195,9 +197,9 @@ "displayRefreshRatePreferHighest": "Highest rate", "displayRefreshRatePreferLowest": "Lowest rate", - "slideshowVideoPlaybackSkip": "Skip", - "slideshowVideoPlaybackMuted": "Play muted", - "slideshowVideoPlaybackWithSound": "Play with sound", + "videoPlaybackSkip": "Skip", + "videoPlaybackMuted": "Play muted", + "videoPlaybackWithSound": "Play with sound", "themeBrightnessLight": "Light", "themeBrightnessDark": "Dark", @@ -207,11 +209,15 @@ "viewerTransitionParallax": "Parallax", "viewerTransitionFade": "Fade", "viewerTransitionZoomIn": "Zoom in", + "viewerTransitionNone": "None", "wallpaperTargetHome": "Home screen", "wallpaperTargetLock": "Lock screen", "wallpaperTargetHomeLock": "Home and lock screens", + "widgetOpenPageHome": "Open home", + "widgetOpenPageViewer": "Open viewer", + "albumTierNew": "New", "albumTierPinned": "Pinned", "albumTierSpecial": "Common", @@ -419,6 +425,7 @@ "viewDialogLayoutSectionTitle": "Layout", "viewDialogReverseSortOrder": "Reverse sort order", + "tileLayoutMosaic": "Mosaic", "tileLayoutGrid": "Grid", "tileLayoutList": "List", @@ -581,9 +588,12 @@ "sortOrderSmallestFirst": "Smallest first", "albumGroupTier": "By tier", + "albumGroupType": "By type", "albumGroupVolume": "By storage volume", "albumGroupNone": "Do not group", + "albumMimeTypeMixed": "Mixed", + "albumPickPageTitleCopy": "Copy to Album", "albumPickPageTitleExport": "Export to Album", "albumPickPageTitleMove": "Move to Album", @@ -620,8 +630,9 @@ "searchMetadataSectionTitle": "Metadata", "settingsPageTitle": "Settings", - "settingsSystemDefault": "System", + "settingsSystemDefault": "System default", "settingsDefault": "Default", + "settingsDisabled": "Disabled", "settingsSearchFieldLabel": "Search settings", "settingsSearchEmpty": "No matching setting", @@ -704,10 +715,9 @@ "settingsSlideshowRepeat": "Repeat", "settingsSlideshowShuffle": "Shuffle", "settingsSlideshowFillScreen": "Fill screen", + "settingsSlideshowAnimatedZoomEffect": "Animated zoom effect", "settingsSlideshowTransitionTile": "Transition", - "settingsSlideshowTransitionDialogTitle": "Transition", "settingsSlideshowIntervalTile": "Interval", - "settingsSlideshowIntervalDialogTitle": "Interval", "settingsSlideshowVideoPlaybackTile": "Video playback", "settingsSlideshowVideoPlaybackDialogTitle": "Video Playback", @@ -715,7 +725,7 @@ "settingsVideoSectionTitle": "Video", "settingsVideoShowVideos": "Show videos", "settingsVideoEnableHardwareAcceleration": "Hardware acceleration", - "settingsVideoEnableAutoPlay": "Auto play", + "settingsVideoAutoPlay": "Auto play", "settingsVideoLoopModeTile": "Loop mode", "settingsVideoLoopModeDialogTitle": "Loop Mode", @@ -737,7 +747,6 @@ "settingsVideoControlsTile": "Controls", "settingsVideoControlsPageTitle": "Controls", "settingsVideoButtonsTile": "Buttons", - "settingsVideoButtonsDialogTitle": "Buttons", "settingsVideoGestureDoubleTapTogglePlay": "Double tap to play/pause", "settingsVideoGestureSideDoubleTapSeek": "Double tap on screen edges to seek backward/forward", @@ -770,7 +779,6 @@ "settingsRemoveAnimationsTile": "Remove animations", "settingsRemoveAnimationsDialogTitle": "Remove Animations", "settingsTimeToTakeActionTile": "Time to take action", - "settingsTimeToTakeActionDialogTitle": "Time to Take Action", "settingsDisplaySectionTitle": "Display", "settingsThemeBrightnessTile": "Theme", @@ -792,6 +800,7 @@ "settingsWidgetPageTitle": "Photo Frame", "settingsWidgetShowOutline": "Outline", + "settingsWidgetOpenPage": "When tapping on the widget", "settingsCollectionTile": "Collection", @@ -805,6 +814,7 @@ "statsTopCountriesSectionTitle": "Top Countries", "statsTopPlacesSectionTitle": "Top Places", "statsTopTagsSectionTitle": "Top Tags", + "statsTopAlbumsSectionTitle": "Top Albums", "viewerOpenPanoramaButtonLabel": "OPEN PANORAMA", "viewerSetWallpaperButtonLabel": "SET WALLPAPER", @@ -849,6 +859,8 @@ "viewerInfoSearchSuggestionResolution": "Resolution", "viewerInfoSearchSuggestionRights": "Rights", + "wallpaperUseScrollEffect": "Use scroll effect on home screen", + "tagEditorPageTitle": "Edit Tags", "tagEditorPageNewTagFieldLabel": "New tag", "tagEditorPageAddTagTooltip": "Add tag", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index e84adbb27..936e600f3 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -151,9 +151,9 @@ "displayRefreshRatePreferHighest": "Alta tasa", "displayRefreshRatePreferLowest": "Baja tasa", - "slideshowVideoPlaybackSkip": "Saltear", - "slideshowVideoPlaybackMuted": "Reproducir sin sonido", - "slideshowVideoPlaybackWithSound": "Reproducir con sonido", + "videoPlaybackSkip": "Saltear", + "videoPlaybackMuted": "Reproducir sin sonido", + "videoPlaybackWithSound": "Reproducir con sonido", "themeBrightnessLight": "Claro", "themeBrightnessDark": "Obscuro", @@ -509,9 +509,7 @@ "settingsSlideshowShuffle": "Mezclar", "settingsSlideshowFillScreen": "Llenar pantalla", "settingsSlideshowTransitionTile": "Transición", - "settingsSlideshowTransitionDialogTitle": "Transición", "settingsSlideshowIntervalTile": "Intervalo", - "settingsSlideshowIntervalDialogTitle": "Intervalo", "settingsSlideshowVideoPlaybackTile": "Reproducción de video", "settingsSlideshowVideoPlaybackDialogTitle": "Reproducción de video", @@ -519,7 +517,7 @@ "settingsVideoSectionTitle": "Video", "settingsVideoShowVideos": "Mostrar videos", "settingsVideoEnableHardwareAcceleration": "Aceleración por hardware", - "settingsVideoEnableAutoPlay": "Reproducción automática", + "settingsVideoAutoPlay": "Reproducción automática", "settingsVideoLoopModeTile": "Modo bucle", "settingsVideoLoopModeDialogTitle": "Modo bucle", @@ -541,7 +539,6 @@ "settingsVideoControlsTile": "Controles", "settingsVideoControlsPageTitle": "Controles", "settingsVideoButtonsTile": "Botones", - "settingsVideoButtonsDialogTitle": "Botones", "settingsVideoGestureDoubleTapTogglePlay": "Doble toque para reproducir/pausar", "settingsVideoGestureSideDoubleTapSeek": "Doble toque en los bordes de la pantalla para buscar atrás/adelante", @@ -574,7 +571,6 @@ "settingsRemoveAnimationsTile": "Remover animaciones", "settingsRemoveAnimationsDialogTitle": "Remover animaciones", "settingsTimeToTakeActionTile": "Retraso para ejecutar una acción", - "settingsTimeToTakeActionDialogTitle": "Retraso para ejecutar una acción", "settingsDisplaySectionTitle": "Pantalla", "settingsThemeBrightnessTile": "Tema", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 67fff4117..4e397da56 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -41,6 +41,8 @@ "chipActionGoToAlbumPage": "Afficher dans Albums", "chipActionGoToCountryPage": "Afficher dans Pays", "chipActionGoToTagPage": "Afficher dans Libellés", + "chipActionFilterOut": "Exclure", + "chipActionFilterIn": "Inclure", "chipActionHide": "Masquer", "chipActionPin": "Épingler", "chipActionUnpin": "Retirer", @@ -155,9 +157,9 @@ "displayRefreshRatePreferHighest": "Fréquence maximale", "displayRefreshRatePreferLowest": "Fréquence minimale", - "slideshowVideoPlaybackSkip": "Passer", - "slideshowVideoPlaybackMuted": "Jouer sans son", - "slideshowVideoPlaybackWithSound": "Jouer avec son", + "videoPlaybackSkip": "Passer", + "videoPlaybackMuted": "Jouer sans son", + "videoPlaybackWithSound": "Jouer avec son", "themeBrightnessLight": "Clair", "themeBrightnessDark": "Sombre", @@ -167,11 +169,15 @@ "viewerTransitionParallax": "Parallaxe", "viewerTransitionFade": "Fondu", "viewerTransitionZoomIn": "Zoom", + "viewerTransitionNone": "Aucune", "wallpaperTargetHome": "Écran d’accueil", "wallpaperTargetLock": "Écran de verrouillage", "wallpaperTargetHomeLock": "Écrans accueil et verrouillage", + "widgetOpenPageHome": "Ouvrir la page d’accueil", + "widgetOpenPageViewer": "Ouvrir la visionneuse", + "albumTierNew": "Nouveaux", "albumTierPinned": "Épinglés", "albumTierSpecial": "Standards", @@ -289,6 +295,7 @@ "viewDialogLayoutSectionTitle": "Vue", "viewDialogReverseSortOrder": "Inverser l’ordre", + "tileLayoutMosaic": "Mosaïque", "tileLayoutGrid": "Grille", "tileLayoutList": "Liste", @@ -401,9 +408,12 @@ "sortOrderSmallestFirst": "Moins larges d’abord", "albumGroupTier": "par importance", + "albumGroupType": "par type", "albumGroupVolume": "par volume de stockage", "albumGroupNone": "ne pas grouper", + "albumMimeTypeMixed": "Mixte", + "albumPickPageTitleCopy": "Copie", "albumPickPageTitleExport": "Export", "albumPickPageTitleMove": "Déplacement", @@ -442,6 +452,7 @@ "settingsPageTitle": "Réglages", "settingsSystemDefault": "Système", "settingsDefault": "Par défaut", + "settingsDisabled": "Désactivé", "settingsSearchFieldLabel": "Recherche de réglages", "settingsSearchEmpty": "Aucun réglage correspondant", @@ -524,10 +535,9 @@ "settingsSlideshowRepeat": "Répéter", "settingsSlideshowShuffle": "Aléatoire", "settingsSlideshowFillScreen": "Remplir l’écran", + "settingsSlideshowAnimatedZoomEffect": "Effet de zoom animé", "settingsSlideshowTransitionTile": "Transition", - "settingsSlideshowTransitionDialogTitle": "Transition", "settingsSlideshowIntervalTile": "Intervalle", - "settingsSlideshowIntervalDialogTitle": "Intervalle", "settingsSlideshowVideoPlaybackTile": "Lecture de vidéos", "settingsSlideshowVideoPlaybackDialogTitle": "Lecture de vidéos", @@ -535,7 +545,7 @@ "settingsVideoSectionTitle": "Vidéo", "settingsVideoShowVideos": "Afficher les vidéos", "settingsVideoEnableHardwareAcceleration": "Accélération matérielle", - "settingsVideoEnableAutoPlay": "Lecture automatique", + "settingsVideoAutoPlay": "Lecture automatique", "settingsVideoLoopModeTile": "Lecture répétée", "settingsVideoLoopModeDialogTitle": "Lecture répétée", @@ -557,7 +567,6 @@ "settingsVideoControlsTile": "Contrôles", "settingsVideoControlsPageTitle": "Contrôles", "settingsVideoButtonsTile": "Boutons", - "settingsVideoButtonsDialogTitle": "Boutons", "settingsVideoGestureDoubleTapTogglePlay": "Appuyer deux fois pour lire ou mettre en pause", "settingsVideoGestureSideDoubleTapSeek": "Appuyer deux fois sur les bords de l’écran pour reculer ou avancer", @@ -590,7 +599,6 @@ "settingsRemoveAnimationsTile": "Suppression des animations", "settingsRemoveAnimationsDialogTitle": "Suppression des animations", "settingsTimeToTakeActionTile": "Délai pour effectuer une action", - "settingsTimeToTakeActionDialogTitle": "Délai pour effectuer une action", "settingsDisplaySectionTitle": "Affichage", "settingsThemeBrightnessTile": "Thème", @@ -612,6 +620,7 @@ "settingsWidgetPageTitle": "Cadre photo", "settingsWidgetShowOutline": "Contours", + "settingsWidgetOpenPage": "Quand vous appuyez sur le widget", "settingsCollectionTile": "Collection", @@ -620,6 +629,7 @@ "statsTopCountriesSectionTitle": "Top pays", "statsTopPlacesSectionTitle": "Top lieux", "statsTopTagsSectionTitle": "Top libellés", + "statsTopAlbumsSectionTitle": "Top albums", "viewerOpenPanoramaButtonLabel": "OUVRIR LE PANORAMA", "viewerSetWallpaperButtonLabel": "APPLIQUER", @@ -664,6 +674,8 @@ "viewerInfoSearchSuggestionResolution": "Résolution", "viewerInfoSearchSuggestionRights": "Droits", + "wallpaperUseScrollEffect": "Utiliser l’effet de défilement sur l’écran d’accueil", + "tagEditorPageTitle": "Modifier les libellés", "tagEditorPageNewTagFieldLabel": "Nouveau libellé", "tagEditorPageAddTagTooltip": "Ajouter le libellé", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 50b866d50..9f8c92407 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -155,9 +155,9 @@ "displayRefreshRatePreferHighest": "Penyegaran tertinggi", "displayRefreshRatePreferLowest": "Penyegaran terendah", - "slideshowVideoPlaybackSkip": "Lewati", - "slideshowVideoPlaybackMuted": "Mainkan bisu", - "slideshowVideoPlaybackWithSound": "Mainkan dengan suara", + "videoPlaybackSkip": "Lewati", + "videoPlaybackMuted": "Mainkan bisu", + "videoPlaybackWithSound": "Mainkan dengan suara", "themeBrightnessLight": "Terang", "themeBrightnessDark": "Gelap", @@ -525,9 +525,7 @@ "settingsSlideshowShuffle": "Acak", "settingsSlideshowFillScreen": "Isi layar", "settingsSlideshowTransitionTile": "Transisi", - "settingsSlideshowTransitionDialogTitle": "Transisi", "settingsSlideshowIntervalTile": "Interval", - "settingsSlideshowIntervalDialogTitle": "Interval", "settingsSlideshowVideoPlaybackTile": "Putaran ulang video", "settingsSlideshowVideoPlaybackDialogTitle": "Putaran Ulang Video", @@ -535,7 +533,7 @@ "settingsVideoSectionTitle": "Video", "settingsVideoShowVideos": "Tampilkan video", "settingsVideoEnableHardwareAcceleration": "Akselerasi perangkat keras", - "settingsVideoEnableAutoPlay": "Putar otomatis", + "settingsVideoAutoPlay": "Putar otomatis", "settingsVideoLoopModeTile": "Putar ulang", "settingsVideoLoopModeDialogTitle": "Putar Ulang", @@ -557,7 +555,6 @@ "settingsVideoControlsTile": "Kontrol", "settingsVideoControlsPageTitle": "Kontrol", "settingsVideoButtonsTile": "Tombol", - "settingsVideoButtonsDialogTitle": "Tombol", "settingsVideoGestureDoubleTapTogglePlay": "Ketuk dua kali untuk mainkan/hentikan", "settingsVideoGestureSideDoubleTapSeek": "Ketuk dua kali di tepi layar untuk mencari kebelakang/kedepan", @@ -590,7 +587,6 @@ "settingsRemoveAnimationsTile": "Hapus animasi", "settingsRemoveAnimationsDialogTitle": "Hapus Animasi", "settingsTimeToTakeActionTile": "Waktu untuk mengambil tindakan", - "settingsTimeToTakeActionDialogTitle": "Saatnya Bertindak", "settingsDisplaySectionTitle": "Tampilan", "settingsThemeBrightnessTile": "Tema", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index c2fe6374a..1e68520d5 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -40,7 +40,9 @@ "chipActionDelete": "Elimina", "chipActionGoToAlbumPage": "Mostra negli album", "chipActionGoToCountryPage": "Mostra nei Paesi", - "chipActionGoToTagPage": "Mostra nelle etichette", + "chipActionGoToTagPage": "Mostra nelle Etichette", + "chipActionFilterOut": "Escludi", + "chipActionFilterIn": "Includi", "chipActionHide": "Nascondi", "chipActionPin": "Fissa in alto", "chipActionUnpin": "Rimuovi dall’alto", @@ -155,9 +157,9 @@ "displayRefreshRatePreferHighest": "Frequenza massima", "displayRefreshRatePreferLowest": "Frequenza minima", - "slideshowVideoPlaybackSkip": "Salta", - "slideshowVideoPlaybackMuted": "Riproduci senza audio", - "slideshowVideoPlaybackWithSound": "Riproduci con audio", + "videoPlaybackSkip": "Salta", + "videoPlaybackMuted": "Riproduci senza audio", + "videoPlaybackWithSound": "Riproduci con audio", "themeBrightnessLight": "Chiaro", "themeBrightnessDark": "Scuro", @@ -167,11 +169,15 @@ "viewerTransitionParallax": "Parallasse", "viewerTransitionFade": "Dissolvenza", "viewerTransitionZoomIn": "Ingrandisci", + "viewerTransitionNone": "Nessuna", "wallpaperTargetHome": "Schermata iniziale", "wallpaperTargetLock": "Schermata di blocco", "wallpaperTargetHomeLock": "Schermata iniziale e di blocco", + "widgetOpenPageHome": "Apri pagina iniziale", + "widgetOpenPageViewer": "Apri visualizzazione", + "albumTierNew": "Nuovi", "albumTierPinned": "Fissati", "albumTierSpecial": "Frequenti", @@ -289,6 +295,7 @@ "viewDialogLayoutSectionTitle": "Layout", "viewDialogReverseSortOrder": "Inverti ordinamento", + "tileLayoutMosaic": "Mosaico", "tileLayoutGrid": "Griglia", "tileLayoutList": "Lista", @@ -390,6 +397,7 @@ "sortBySize": "Per dimensione", "sortByAlbumFileName": "Per album e nome del file", "sortByRating": "Per valutazione", + "sortOrderNewestFirst": "Prima i più nuovi", "sortOrderOldestFirst": "Prima i più vecchi", "sortOrderAtoZ": "Dalla A alla Z", @@ -400,9 +408,12 @@ "sortOrderSmallestFirst": "Prima i più piccoli", "albumGroupTier": "Per importanza", + "albumGroupType": "Per tipo", "albumGroupVolume": "Per volume di archiviazione", "albumGroupNone": "Non raggruppare", + "albumMimeTypeMixed": "Misto", + "albumPickPageTitleCopy": "Copia", "albumPickPageTitleExport": "Esporta", "albumPickPageTitleMove": "Sposta", @@ -441,6 +452,7 @@ "settingsPageTitle": "Impostazioni", "settingsSystemDefault": "Sistema", "settingsDefault": "Predefinite", + "settingsDisabled": "Disabilitato", "settingsSearchFieldLabel": "Ricerca impostazioni", "settingsSearchEmpty": "Nessuna impostazione corrispondente", @@ -523,10 +535,9 @@ "settingsSlideshowRepeat": "Ripeti", "settingsSlideshowShuffle": "Ordine casuale", "settingsSlideshowFillScreen": "Riempi schermo", + "settingsSlideshowAnimatedZoomEffect": "Effetto ingrandimento animato", "settingsSlideshowTransitionTile": "Transizione", - "settingsSlideshowTransitionDialogTitle": "Transizione", "settingsSlideshowIntervalTile": "Intervallo", - "settingsSlideshowIntervalDialogTitle": "Intervallo", "settingsSlideshowVideoPlaybackTile": "Riproduzione video", "settingsSlideshowVideoPlaybackDialogTitle": "Riproduzione video", @@ -534,7 +545,7 @@ "settingsVideoSectionTitle": "Video", "settingsVideoShowVideos": "Mostra video", "settingsVideoEnableHardwareAcceleration": "Accelerazione hardware", - "settingsVideoEnableAutoPlay": "Riproduzione automatica", + "settingsVideoAutoPlay": "Riproduzione automatica", "settingsVideoLoopModeTile": "Modalità loop", "settingsVideoLoopModeDialogTitle": "Modalità loop", @@ -556,7 +567,6 @@ "settingsVideoControlsTile": "Controlli", "settingsVideoControlsPageTitle": "Controlli", "settingsVideoButtonsTile": "Pulsanti", - "settingsVideoButtonsDialogTitle": "Pulsanti", "settingsVideoGestureDoubleTapTogglePlay": "Doppio tocco per play/pausa", "settingsVideoGestureSideDoubleTapSeek": "Doppio tocco sui bordi dello schermo per cercare avanti/indietro", @@ -589,7 +599,6 @@ "settingsRemoveAnimationsTile": "Rimuovi animazioni", "settingsRemoveAnimationsDialogTitle": "Rimuovi animazioni", "settingsTimeToTakeActionTile": "Tempo di reazione", - "settingsTimeToTakeActionDialogTitle": "Tempo di reazione", "settingsDisplaySectionTitle": "Schermo", "settingsThemeBrightnessTile": "Tema", @@ -611,6 +620,7 @@ "settingsWidgetPageTitle": "Cornice foto", "settingsWidgetShowOutline": "Contorno", + "settingsWidgetOpenPage": "Se tocchi il widget", "settingsCollectionTile": "Collezione", @@ -619,6 +629,7 @@ "statsTopCountriesSectionTitle": "Paesi più frequenti", "statsTopPlacesSectionTitle": "Luoghi più frequenti", "statsTopTagsSectionTitle": "Etichette più frequenti", + "statsTopAlbumsSectionTitle": "Album più frequenti", "viewerOpenPanoramaButtonLabel": "APRI PANORAMA", "viewerSetWallpaperButtonLabel": "IMPOSTA SFONDO", @@ -663,6 +674,8 @@ "viewerInfoSearchSuggestionResolution": "Risoluzione", "viewerInfoSearchSuggestionRights": "Diritti", + "wallpaperUseScrollEffect": "Usa effetto di scorrimento nella schermata iniziale", + "tagEditorPageTitle": "Modifica etichette", "tagEditorPageNewTagFieldLabel": "Nuova etichetta", "tagEditorPageAddTagTooltip": "Aggiungi etichetta", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index a65a58789..5304060f4 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -151,9 +151,9 @@ "displayRefreshRatePreferHighest": "高レート", "displayRefreshRatePreferLowest": "低レート", - "slideshowVideoPlaybackSkip": "スキップ", - "slideshowVideoPlaybackMuted": "ミュート再生", - "slideshowVideoPlaybackWithSound": "音声あり再生", + "videoPlaybackSkip": "スキップ", + "videoPlaybackMuted": "ミュート再生", + "videoPlaybackWithSound": "音声あり再生", "themeBrightnessLight": "ライト", "themeBrightnessDark": "ダーク", @@ -508,9 +508,7 @@ "settingsSlideshowShuffle": "シャッフル", "settingsSlideshowFillScreen": "画面いっぱいに表示", "settingsSlideshowTransitionTile": "トランジション", - "settingsSlideshowTransitionDialogTitle": "トランジション", "settingsSlideshowIntervalTile": "間隔", - "settingsSlideshowIntervalDialogTitle": "間隔", "settingsSlideshowVideoPlaybackTile": "動画を再生", "settingsSlideshowVideoPlaybackDialogTitle": "動画再生", @@ -518,7 +516,7 @@ "settingsVideoSectionTitle": "動画", "settingsVideoShowVideos": "動画を表示", "settingsVideoEnableHardwareAcceleration": "ハードウェア アクセラレーション", - "settingsVideoEnableAutoPlay": "自動再生", + "settingsVideoAutoPlay": "自動再生", "settingsVideoLoopModeTile": "ループ モード", "settingsVideoLoopModeDialogTitle": "ループ モード", @@ -540,7 +538,6 @@ "settingsVideoControlsTile": "操作", "settingsVideoControlsPageTitle": "操作", "settingsVideoButtonsTile": "ボタン", - "settingsVideoButtonsDialogTitle": "ボタン", "settingsVideoGestureDoubleTapTogglePlay": "2回タップして再生/一時停止", "settingsVideoGestureSideDoubleTapSeek": "画面の角を2回タップして早送り/早戻し", @@ -573,7 +570,6 @@ "settingsRemoveAnimationsTile": "アニメーションの削除", "settingsRemoveAnimationsDialogTitle": "アニメーションの削除", "settingsTimeToTakeActionTile": "操作までの時間", - "settingsTimeToTakeActionDialogTitle": "操作までの時間", "settingsDisplaySectionTitle": "ディスプレイ", "settingsThemeBrightnessTile": "テーマ", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index ea9379b64..64dc52ed8 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -41,6 +41,8 @@ "chipActionGoToAlbumPage": "앨범 페이지에서 보기", "chipActionGoToCountryPage": "국가 페이지에서 보기", "chipActionGoToTagPage": "태그 페이지에서 보기", + "chipActionFilterOut": "제외하기", + "chipActionFilterIn": "포함시키기", "chipActionHide": "숨기기", "chipActionPin": "고정", "chipActionUnpin": "고정 해제", @@ -155,9 +157,9 @@ "displayRefreshRatePreferHighest": "가장 높은 재생률", "displayRefreshRatePreferLowest": "가장 낮은 재생률", - "slideshowVideoPlaybackSkip": "생략", - "slideshowVideoPlaybackMuted": "음소거 재생", - "slideshowVideoPlaybackWithSound": "일반 재생", + "videoPlaybackSkip": "생략", + "videoPlaybackMuted": "음소거 재생", + "videoPlaybackWithSound": "일반 재생", "themeBrightnessLight": "라이트", "themeBrightnessDark": "다크", @@ -167,11 +169,15 @@ "viewerTransitionParallax": "시차", "viewerTransitionFade": "페이드", "viewerTransitionZoomIn": "확대", + "viewerTransitionNone": "없음", "wallpaperTargetHome": "홈 화면", "wallpaperTargetLock": "잠금화면", "wallpaperTargetHomeLock": "홈 및 잠금화면", + "widgetOpenPageHome": "홈 열기", + "widgetOpenPageViewer": "뷰어 열기", + "albumTierNew": "신규", "albumTierPinned": "고정", "albumTierSpecial": "기본", @@ -289,6 +295,7 @@ "viewDialogLayoutSectionTitle": "배치", "viewDialogReverseSortOrder": "순서를 뒤바꾸기", + "tileLayoutMosaic": "모자이크", "tileLayoutGrid": "바둑판", "tileLayoutList": "목록", @@ -401,9 +408,12 @@ "sortOrderSmallestFirst": "작은 파일순", "albumGroupTier": "단계별로", + "albumGroupType": "유형별로", "albumGroupVolume": "저장공간별로", "albumGroupNone": "묶음 없음", + "albumMimeTypeMixed": "혼합", + "albumPickPageTitleCopy": "앨범으로 복사", "albumPickPageTitleExport": "앨범으로 내보내기", "albumPickPageTitleMove": "앨범으로 이동", @@ -440,8 +450,9 @@ "searchMetadataSectionTitle": "메타데이터", "settingsPageTitle": "설정", - "settingsSystemDefault": "시스템", + "settingsSystemDefault": "시스템 기본값", "settingsDefault": "기본", + "settingsDisabled": "사용 안함", "settingsSearchFieldLabel": "설정 검색", "settingsSearchEmpty": "결과가 없습니다", @@ -524,10 +535,9 @@ "settingsSlideshowRepeat": "반복", "settingsSlideshowShuffle": "순서섞기", "settingsSlideshowFillScreen": "화면 채우기", + "settingsSlideshowAnimatedZoomEffect": "애니메이션 확대/축소 효과", "settingsSlideshowTransitionTile": "전환 효과", - "settingsSlideshowTransitionDialogTitle": "전환 효과", "settingsSlideshowIntervalTile": "교체 주기", - "settingsSlideshowIntervalDialogTitle": "교체 주기", "settingsSlideshowVideoPlaybackTile": "동영상 재생", "settingsSlideshowVideoPlaybackDialogTitle": "동영상 재생", @@ -535,7 +545,7 @@ "settingsVideoSectionTitle": "동영상", "settingsVideoShowVideos": "미디어에 동영상 표시", "settingsVideoEnableHardwareAcceleration": "하드웨어 가속", - "settingsVideoEnableAutoPlay": "자동 재생", + "settingsVideoAutoPlay": "자동 재생", "settingsVideoLoopModeTile": "반복 모드", "settingsVideoLoopModeDialogTitle": "반복 모드", @@ -557,7 +567,6 @@ "settingsVideoControlsTile": "제어", "settingsVideoControlsPageTitle": "제어", "settingsVideoButtonsTile": "버튼", - "settingsVideoButtonsDialogTitle": "버튼", "settingsVideoGestureDoubleTapTogglePlay": "두 번 탭해서 재생이나 일시정지하기", "settingsVideoGestureSideDoubleTapSeek": "화면 측면에서 두 번 탭해서 앞뒤로 가기", @@ -590,7 +599,6 @@ "settingsRemoveAnimationsTile": "애니메이션 삭제", "settingsRemoveAnimationsDialogTitle": "애니메이션 삭제", "settingsTimeToTakeActionTile": "액션 취하기 전 대기 시간", - "settingsTimeToTakeActionDialogTitle": "액션 취하기 전 대기 시간", "settingsDisplaySectionTitle": "디스플레이", "settingsThemeBrightnessTile": "테마", @@ -612,6 +620,7 @@ "settingsWidgetPageTitle": "사진 액자", "settingsWidgetShowOutline": "윤곽", + "settingsWidgetOpenPage": "위젯을 탭하면", "settingsCollectionTile": "미디어", @@ -620,6 +629,7 @@ "statsTopCountriesSectionTitle": "국가 랭킹", "statsTopPlacesSectionTitle": "장소 랭킹", "statsTopTagsSectionTitle": "태그 랭킹", + "statsTopAlbumsSectionTitle": "앨범 랭킹", "viewerOpenPanoramaButtonLabel": "파노라마 열기", "viewerSetWallpaperButtonLabel": "설정", @@ -664,6 +674,8 @@ "viewerInfoSearchSuggestionResolution": "해상도", "viewerInfoSearchSuggestionRights": "권리", + "wallpaperUseScrollEffect": "홈 화면에 스크롤 효과 사용", + "tagEditorPageTitle": "태그 수정", "tagEditorPageNewTagFieldLabel": "새 태그", "tagEditorPageAddTagTooltip": "태그 추가", diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 4105b5218..2433598cd 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -41,6 +41,8 @@ "chipActionGoToAlbumPage": "Tonen Albums", "chipActionGoToCountryPage": "Tonen in Landen", "chipActionGoToTagPage": "Tonen in Labels", + "chipActionFilterOut": "Uitfilteren", + "chipActionFilterIn": "Infilteren", "chipActionHide": "Verbergen", "chipActionPin": "Bovenaan pinnen", "chipActionUnpin": "Unpinnen", @@ -155,9 +157,9 @@ "displayRefreshRatePreferHighest": "Hoogste waardering", "displayRefreshRatePreferLowest": "Laagste waardering", - "slideshowVideoPlaybackSkip": "Overslaan", - "slideshowVideoPlaybackMuted": "Gedempte afspelen", - "slideshowVideoPlaybackWithSound": "Met geluid afspelen", + "videoPlaybackSkip": "Overslaan", + "videoPlaybackMuted": "Gedempte afspelen", + "videoPlaybackWithSound": "Met geluid afspelen", "themeBrightnessLight": "Licht", "themeBrightnessDark": "Donker", @@ -167,11 +169,15 @@ "viewerTransitionParallax": "Parallax", "viewerTransitionFade": "Vervagen", "viewerTransitionZoomIn": "Inzoomen", + "viewerTransitionNone": "Geen", "wallpaperTargetHome": "Home scherm", "wallpaperTargetLock": "Vergrendel scherm", "wallpaperTargetHomeLock": "Home and Vergrendel schermen", + "widgetOpenPageHome": "Open startscherm", + "widgetOpenPageViewer": "Open viewer", + "albumTierNew": "Nieuw", "albumTierPinned": "Gepint", "albumTierSpecial": "Veelgebruikt", @@ -289,6 +295,7 @@ "viewDialogLayoutSectionTitle": "Layout", "viewDialogReverseSortOrder": "Draai sorteerrichting om", + "tileLayoutMosaic": "Mozaïek", "tileLayoutGrid": "Raster", "tileLayoutList": "Lijst", @@ -401,9 +408,12 @@ "sortOrderSmallestFirst": "Kleinste eerst", "albumGroupTier": "Op rang", + "albumGroupType": "Op type", "albumGroupVolume": "Op opslagvolume", "albumGroupNone": "Niet groeperen", + "albumMimeTypeMixed": "Gemengd", + "albumPickPageTitleCopy": "Kopieer naar Album", "albumPickPageTitleExport": "Exporteer naar Album", "albumPickPageTitleMove": "Verplaats naar Album", @@ -442,6 +452,7 @@ "settingsPageTitle": "Instellingen", "settingsSystemDefault": "Systeem", "settingsDefault": "Standaard", + "settingsDisabled": "Uitgeschakeld", "settingsSearchFieldLabel": "Instellingen doorzoeken", "settingsSearchEmpty": "Geen instellingen gevonden", @@ -524,10 +535,9 @@ "settingsSlideshowRepeat": "Herhalen", "settingsSlideshowShuffle": "Shuffle", "settingsSlideshowFillScreen": "Volledig scherm", + "settingsSlideshowAnimatedZoomEffect": "Geanimeerd zoomeffect", "settingsSlideshowTransitionTile": "Overgang", - "settingsSlideshowTransitionDialogTitle": "Overgang", "settingsSlideshowIntervalTile": "Interval", - "settingsSlideshowIntervalDialogTitle": "Interval", "settingsSlideshowVideoPlaybackTile": "Video afspelen", "settingsSlideshowVideoPlaybackDialogTitle": "Video afspelen", @@ -535,7 +545,7 @@ "settingsVideoSectionTitle": "Video", "settingsVideoShowVideos": "Videos", "settingsVideoEnableHardwareAcceleration": "Hardware acceleratie", - "settingsVideoEnableAutoPlay": "Automatisch afspelen", + "settingsVideoAutoPlay": "Automatisch afspelen", "settingsVideoLoopModeTile": "Herhaald afspelen", "settingsVideoLoopModeDialogTitle": "Herhaald afspelen", @@ -557,7 +567,6 @@ "settingsVideoControlsTile": "Bediening", "settingsVideoControlsPageTitle": "Bediening", "settingsVideoButtonsTile": "Knoppen", - "settingsVideoButtonsDialogTitle": "Knoppen", "settingsVideoGestureDoubleTapTogglePlay": "Dubbeltik om te spelen/pauzeren", "settingsVideoGestureSideDoubleTapSeek": "Dubbeltik op schermranden om achteruit/vooruit te zoeken", @@ -590,7 +599,6 @@ "settingsRemoveAnimationsTile": "Animaties verwijderen", "settingsRemoveAnimationsDialogTitle": "Animaties verwijderen", "settingsTimeToTakeActionTile": "Tijd om actie te ondernemen", - "settingsTimeToTakeActionDialogTitle": "Tijd om actie te ondernemen", "settingsDisplaySectionTitle": "Scherm", "settingsThemeBrightnessTile": "Thema", @@ -612,6 +620,7 @@ "settingsWidgetPageTitle": "Foto Lijstje", "settingsWidgetShowOutline": "Contour", + "settingsWidgetOpenPage": "Wanneer u op de widget tikt", "settingsCollectionTile": "Verzameling", @@ -620,6 +629,7 @@ "statsTopCountriesSectionTitle": "Top Landen", "statsTopPlacesSectionTitle": "Top Plaatsen", "statsTopTagsSectionTitle": "Top Labels", + "statsTopAlbumsSectionTitle": "Top Albums", "viewerOpenPanoramaButtonLabel": "OPEN PANORAMA", "viewerSetWallpaperButtonLabel": "ALS ACHTERGROND INSTELLEN", @@ -664,6 +674,8 @@ "viewerInfoSearchSuggestionResolution": "Resolutie", "viewerInfoSearchSuggestionRights": "Rechten", + "wallpaperUseScrollEffect": "Scroll-effect gebruiken op startscherm", + "tagEditorPageTitle": "Wijzig Labels", "tagEditorPageNewTagFieldLabel": "Nieuw label", "tagEditorPageAddTagTooltip": "Label toevoegen", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index f70c7674c..a13b8e424 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -41,6 +41,8 @@ "chipActionGoToAlbumPage": "Mostrar nos Álbuns", "chipActionGoToCountryPage": "Mostrar em Países", "chipActionGoToTagPage": "Mostrar em Etiquetas", + "chipActionFilterOut": "Filtrar dentro", + "chipActionFilterIn": "Filtrar fora", "chipActionHide": "Ocultar", "chipActionPin": "Fixar no topo", "chipActionUnpin": "Desafixar do topo", @@ -88,15 +90,18 @@ "entryInfoActionEditDate": "Editar data e hora", "entryInfoActionEditLocation": "Editar localização", + "entryInfoActionEditTitleDescription": "Editar título e descrição", "entryInfoActionEditRating": "Editar classificação", "entryInfoActionEditTags": "Editar etiquetas", "entryInfoActionRemoveMetadata": "Remover metadados", "filterBinLabel": "Lixeira", "filterFavouriteLabel": "Favorito", + "filterNoDateLabel": "Sem data", "filterNoLocationLabel": "Não localizado", "filterNoRatingLabel": "Sem classificação", "filterNoTagLabel": "Sem etiqueta", + "filterNoTitleLabel": "Sem título", "filterOnThisDayLabel": "Neste dia", "filterRecentlyAddedLabel": "Adicionado recentemente", "filterRatingRejectedLabel": "Rejeitado", @@ -152,9 +157,9 @@ "displayRefreshRatePreferHighest": "Taxa mais alta", "displayRefreshRatePreferLowest": "Taxa mais baixa", - "slideshowVideoPlaybackSkip": "Pular", - "slideshowVideoPlaybackMuted": "Reproduzir sem som", - "slideshowVideoPlaybackWithSound": "Reproduzir com som", + "videoPlaybackSkip": "Pular", + "videoPlaybackMuted": "Reproduzir sem som", + "videoPlaybackWithSound": "Reproduzir com som", "themeBrightnessLight": "Claro", "themeBrightnessDark": "Escuro", @@ -164,11 +169,15 @@ "viewerTransitionParallax": "Parallax", "viewerTransitionFade": "Desvaneça", "viewerTransitionZoomIn": "Mais zoom", + "viewerTransitionNone": "Nenhum", "wallpaperTargetHome": "Tela inicial", "wallpaperTargetLock": "Tela de bloqueio", "wallpaperTargetHomeLock": "Telas iniciais e de bloqueio", + "widgetOpenPageHome": "Abrir inicial", + "widgetOpenPageViewer": "Abrir visualizador", + "albumTierNew": "Novo", "albumTierPinned": "Fixada", "albumTierSpecial": "Comum", @@ -284,7 +293,9 @@ "viewDialogSortSectionTitle": "Organizar", "viewDialogGroupSectionTitle": "Grupo", "viewDialogLayoutSectionTitle": "Layout", + "viewDialogReverseSortOrder": "Ordem de classificação inversa", + "tileLayoutMosaic": "Mosaico", "tileLayoutGrid": "Grid", "tileLayoutList": "Lista", @@ -387,10 +398,22 @@ "sortByAlbumFileName": "Por álbum e nome de arquivo", "sortByRating": "Por classificação", + "sortOrderNewestFirst": "Os mais novos primeiro", + "sortOrderOldestFirst": "Mais velhos primeiro", + "sortOrderAtoZ": "A a Z", + "sortOrderZtoA": "Z a A", + "sortOrderHighestFirst": "Mais alto primeiro", + "sortOrderLowestFirst": "Mais baixo primeiro", + "sortOrderLargestFirst": "Maior primeiro", + "sortOrderSmallestFirst": "Menor primeiro", + + "albumGroupType": "Por tipo", "albumGroupTier": "Por nível", "albumGroupVolume": "Por volume de armazenamento", "albumGroupNone": "Não agrupe", + "albumMimeTypeMixed": "Misturado", + "albumPickPageTitleCopy": "Copiar para o álbum", "albumPickPageTitleExport": "Exportar para o álbum", "albumPickPageTitleMove": "Mover para o álbum", @@ -424,10 +447,12 @@ "searchPlacesSectionTitle": "Locais", "searchTagsSectionTitle": "Etiquetas", "searchRatingSectionTitle": "Classificações", + "searchMetadataSectionTitle": "Metadados", "settingsPageTitle": "Configurações", "settingsSystemDefault": "Sistema", "settingsDefault": "Padrão", + "settingsDisabled": "Desativado", "settingsSearchFieldLabel": "Pesquisar configuração", "settingsSearchEmpty": "Nenhuma configuração correspondente", @@ -510,10 +535,9 @@ "settingsSlideshowRepeat": "Repetir", "settingsSlideshowShuffle": "Embaralhar", "settingsSlideshowFillScreen": "Preencher tela", + "settingsSlideshowAnimatedZoomEffect": "Efeito de zoom animado", "settingsSlideshowTransitionTile": "Transição", - "settingsSlideshowTransitionDialogTitle": "Transição", "settingsSlideshowIntervalTile": "Intervalo", - "settingsSlideshowIntervalDialogTitle": "Intervalo", "settingsSlideshowVideoPlaybackTile": "Reprodução de vídeo", "settingsSlideshowVideoPlaybackDialogTitle": "Reprodução de vídeo", @@ -521,7 +545,7 @@ "settingsVideoSectionTitle": "Vídeo", "settingsVideoShowVideos": "Mostrar vídeos", "settingsVideoEnableHardwareAcceleration": "Aceleraçao do hardware", - "settingsVideoEnableAutoPlay": "Reprodução automática", + "settingsVideoAutoPlay": "Reprodução automática", "settingsVideoLoopModeTile": "Modo de loop", "settingsVideoLoopModeDialogTitle": "Modo de loop", @@ -543,7 +567,6 @@ "settingsVideoControlsTile": "Controles", "settingsVideoControlsPageTitle": "Controles", "settingsVideoButtonsTile": "Botões", - "settingsVideoButtonsDialogTitle": "Botões", "settingsVideoGestureDoubleTapTogglePlay": "Toque duas vezes para reproduzir/pausar", "settingsVideoGestureSideDoubleTapSeek": "Toque duas vezes nas bordas da tela buscar para trás/para frente", @@ -576,7 +599,6 @@ "settingsRemoveAnimationsTile": "Remover animações", "settingsRemoveAnimationsDialogTitle": "Remover Animações", "settingsTimeToTakeActionTile": "Tempo para executar uma ação", - "settingsTimeToTakeActionDialogTitle": "Tempo para executar uma ação", "settingsDisplaySectionTitle": "Tela", "settingsThemeBrightnessTile": "Tema", @@ -598,6 +620,7 @@ "settingsWidgetPageTitle": "Porta-retratos", "settingsWidgetShowOutline": "Contorno", + "settingsWidgetOpenPage": "Ao tocar no widget", "settingsCollectionTile": "Coleção", @@ -606,6 +629,7 @@ "statsTopCountriesSectionTitle": "Principais Países", "statsTopPlacesSectionTitle": "Principais Lugares", "statsTopTagsSectionTitle": "Principais Etiquetas", + "statsTopAlbumsSectionTitle": "Principais Álbuns", "viewerOpenPanoramaButtonLabel": "ABRIR PANORAMA", "viewerSetWallpaperButtonLabel": "DEFINIR PAPEL DE PAREDE", @@ -650,6 +674,8 @@ "viewerInfoSearchSuggestionResolution": "Resolução", "viewerInfoSearchSuggestionRights": "Direitos", + "wallpaperUseScrollEffect": "Use o efeito de rolagem na tela inicial", + "tagEditorPageTitle": "Editar etiquetas", "tagEditorPageNewTagFieldLabel": "Nova etiqueta", "tagEditorPageAddTagTooltip": "Adicionar etiqueta", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index d6c29cb27..d82fbe696 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -41,6 +41,8 @@ "chipActionGoToAlbumPage": "Показать в Альбомах", "chipActionGoToCountryPage": "Показать в Странах", "chipActionGoToTagPage": "Показать в тегах", + "chipActionFilterOut": "Исключить", + "chipActionFilterIn": "Включить", "chipActionHide": "Скрыть", "chipActionPin": "Закрепить", "chipActionUnpin": "Открепить", @@ -155,9 +157,9 @@ "displayRefreshRatePreferHighest": "Наивысшая частота", "displayRefreshRatePreferLowest": "Наименьшая частота", - "slideshowVideoPlaybackSkip": "Пропустить", - "slideshowVideoPlaybackMuted": "Играть без звука", - "slideshowVideoPlaybackWithSound": "Играть со звуком", + "videoPlaybackSkip": "Пропустить", + "videoPlaybackMuted": "Играть без звука", + "videoPlaybackWithSound": "Играть со звуком", "themeBrightnessLight": "Светлая", "themeBrightnessDark": "Тёмная", @@ -167,11 +169,15 @@ "viewerTransitionParallax": "Параллакс", "viewerTransitionFade": "Затухание", "viewerTransitionZoomIn": "Приближение", + "viewerTransitionNone": "Нет", "wallpaperTargetHome": "Домашний экран", "wallpaperTargetLock": "Экран блокировки", "wallpaperTargetHomeLock": "Домашний экран и экран блокировки", + "widgetOpenPageHome": "Открыть главную страницу", + "widgetOpenPageViewer": "Просмотр текущего", + "albumTierNew": "Новые", "albumTierPinned": "Закрепленные", "albumTierSpecial": "Стандартные", @@ -289,6 +295,7 @@ "viewDialogLayoutSectionTitle": "Макет", "viewDialogReverseSortOrder": "Обратный порядок сортировки", + "tileLayoutMosaic": "Мозайка", "tileLayoutGrid": "Сетка", "tileLayoutList": "Список", @@ -401,9 +408,12 @@ "sortOrderSmallestFirst": "Сначала маленькие", "albumGroupTier": "По уровню", + "albumGroupType": "По типу", "albumGroupVolume": "По накопителю", "albumGroupNone": "Не группировать", + "albumMimeTypeMixed": "Разное", + "albumPickPageTitleCopy": "Копировать в альбом", "albumPickPageTitleExport": "Экспорт в альбом", "albumPickPageTitleMove": "Переместить в альбом", @@ -442,6 +452,7 @@ "settingsPageTitle": "Настройки", "settingsSystemDefault": "Система", "settingsDefault": "По умолчанию", + "settingsDisabled": "Выключено", "settingsSearchFieldLabel": "Поиск настроек", "settingsSearchEmpty": "Нет соответствующих настроек", @@ -524,10 +535,9 @@ "settingsSlideshowRepeat": "Повтор", "settingsSlideshowShuffle": "Вперемешку", "settingsSlideshowFillScreen": "Полный экран", + "settingsSlideshowAnimatedZoomEffect": "Анимация зум эффекта", "settingsSlideshowTransitionTile": "Эффект перехода", - "settingsSlideshowTransitionDialogTitle": "Эффект Перехода", "settingsSlideshowIntervalTile": "Интервал", - "settingsSlideshowIntervalDialogTitle": "Интервал", "settingsSlideshowVideoPlaybackTile": "Проигрывание видео", "settingsSlideshowVideoPlaybackDialogTitle": "Проигрывание Видео", @@ -535,7 +545,7 @@ "settingsVideoSectionTitle": "Видео", "settingsVideoShowVideos": "Показать видео", "settingsVideoEnableHardwareAcceleration": "Аппаратное ускорение", - "settingsVideoEnableAutoPlay": "Автозапуск воспроизведения", + "settingsVideoAutoPlay": "Автозапуск воспроизведения", "settingsVideoLoopModeTile": "Циклический режим", "settingsVideoLoopModeDialogTitle": "Цикличный режим", @@ -557,7 +567,6 @@ "settingsVideoControlsTile": "Элементы управления", "settingsVideoControlsPageTitle": "Элементы управления", "settingsVideoButtonsTile": "Кнопки", - "settingsVideoButtonsDialogTitle": "Кнопки", "settingsVideoGestureDoubleTapTogglePlay": "Двойное нажатие для воспроизведения/паузы", "settingsVideoGestureSideDoubleTapSeek": "Двойное нажатие на края экрана для перехода назад/вперёд", @@ -590,7 +599,6 @@ "settingsRemoveAnimationsTile": "Удалить анимацию", "settingsRemoveAnimationsDialogTitle": "Удалить анимацию", "settingsTimeToTakeActionTile": "Время на выполнение действия", - "settingsTimeToTakeActionDialogTitle": "Время на выполнение действия", "settingsDisplaySectionTitle": "Отображение", "settingsThemeBrightnessTile": "Тема", @@ -612,6 +620,7 @@ "settingsWidgetPageTitle": "Фоторамка", "settingsWidgetShowOutline": "Выделение", + "settingsWidgetOpenPage": "При нажатии на виджет", "settingsCollectionTile": "Коллекция", @@ -620,6 +629,7 @@ "statsTopCountriesSectionTitle": "Топ стран", "statsTopPlacesSectionTitle": "Топ локаций", "statsTopTagsSectionTitle": "Топ тегов", + "statsTopAlbumsSectionTitle": "Топ альбомов", "viewerOpenPanoramaButtonLabel": "ОТКРЫТЬ ПАНОРАМУ", "viewerSetWallpaperButtonLabel": "УСТАНОВИТЬ КАК ОБОИ", @@ -664,6 +674,8 @@ "viewerInfoSearchSuggestionResolution": "Разрешение", "viewerInfoSearchSuggestionRights": "Права", + "wallpaperUseScrollEffect": "Эффект прокрутки на домашнем экране", + "tagEditorPageTitle": "Изменить теги", "tagEditorPageNewTagFieldLabel": "Новый тег", "tagEditorPageAddTagTooltip": "Добавить тег", diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 1dc670844..51da7c382 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -502,7 +502,7 @@ "settingsVideoSectionTitle": "Video", "settingsVideoShowVideos": "Videoları göster", "settingsVideoEnableHardwareAcceleration": "Donanım hızlandırma", - "settingsVideoEnableAutoPlay": "Otomatik oynat", + "settingsVideoAutoPlay": "Otomatik oynat", "settingsVideoLoopModeTile": "Döngü modu", "settingsVideoLoopModeDialogTitle": "Döngü Modu", @@ -524,7 +524,6 @@ "settingsVideoControlsTile": "Kontroller", "settingsVideoControlsPageTitle": "Kontroller", "settingsVideoButtonsTile": "Düğmeler", - "settingsVideoButtonsDialogTitle": "Düğmeler", "settingsVideoGestureDoubleTapTogglePlay": "Oynatmak/duraklatmak için çift dokunun", "settingsVideoGestureSideDoubleTapSeek": "Geri/ileri aramak için ekran kenarlarına çift dokunun", @@ -557,7 +556,6 @@ "settingsRemoveAnimationsTile": "Animasyonları kaldır", "settingsRemoveAnimationsDialogTitle": "Animasyonları Kaldır", "settingsTimeToTakeActionTile": "Harekete geçme zamanı", - "settingsTimeToTakeActionDialogTitle": "Harekete Geçme Zamanı", "settingsDisplaySectionTitle": "Ekran", "settingsThemeBrightnessTile": "Tema", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 539f42cd0..234b46dfc 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -41,6 +41,8 @@ "chipActionGoToAlbumPage": "在相册中显示", "chipActionGoToCountryPage": "在国家中显示", "chipActionGoToTagPage": "在标签中显示", + "chipActionFilterOut": "滤除", + "chipActionFilterIn": "筛选", "chipActionHide": "隐藏", "chipActionPin": "置顶", "chipActionUnpin": "取消置顶", @@ -155,9 +157,9 @@ "displayRefreshRatePreferHighest": "最高刷新率", "displayRefreshRatePreferLowest": "最低刷新率", - "slideshowVideoPlaybackSkip": "跳过", - "slideshowVideoPlaybackMuted": "静音播放", - "slideshowVideoPlaybackWithSound": "带音播放", + "videoPlaybackSkip": "跳过", + "videoPlaybackMuted": "静音播放", + "videoPlaybackWithSound": "带音播放", "themeBrightnessLight": "浅色", "themeBrightnessDark": "深色", @@ -167,11 +169,15 @@ "viewerTransitionParallax": "视差滚动", "viewerTransitionFade": "淡入淡出", "viewerTransitionZoomIn": "放大", + "viewerTransitionNone": "无", "wallpaperTargetHome": "主屏幕", "wallpaperTargetLock": "锁屏界面", "wallpaperTargetHomeLock": "主屏幕 + 锁屏界面", + "widgetOpenPageHome": "打开主页", + "widgetOpenPageViewer": "打开查看器", + "albumTierNew": "新的", "albumTierPinned": "钉选", "albumTierSpecial": "普通", @@ -289,6 +295,7 @@ "viewDialogLayoutSectionTitle": "布局", "viewDialogReverseSortOrder": "反向排序", + "tileLayoutMosaic": "马赛克", "tileLayoutGrid": "网格", "tileLayoutList": "列表", @@ -401,9 +408,12 @@ "sortOrderSmallestFirst": "由小到大", "albumGroupTier": "按层级", + "albumGroupType": "按类型", "albumGroupVolume": "按存储卷", "albumGroupNone": "不分组", + "albumMimeTypeMixed": "混合", + "albumPickPageTitleCopy": "复制到相册", "albumPickPageTitleExport": "导出到相册", "albumPickPageTitleMove": "移至相册", @@ -442,6 +452,7 @@ "settingsPageTitle": "设置", "settingsSystemDefault": "系统", "settingsDefault": "默认", + "settingsDisabled": "禁用", "settingsSearchFieldLabel": "搜索设置", "settingsSearchEmpty": "无匹配设置项", @@ -524,10 +535,9 @@ "settingsSlideshowRepeat": "重复", "settingsSlideshowShuffle": "随机播放", "settingsSlideshowFillScreen": "填充屏幕", + "settingsSlideshowAnimatedZoomEffect": "动画缩放效果", "settingsSlideshowTransitionTile": "过渡动画", - "settingsSlideshowTransitionDialogTitle": "过渡动画", "settingsSlideshowIntervalTile": "时间间隔", - "settingsSlideshowIntervalDialogTitle": "时间间隔", "settingsSlideshowVideoPlaybackTile": "视频回放", "settingsSlideshowVideoPlaybackDialogTitle": "视频回放", @@ -535,7 +545,7 @@ "settingsVideoSectionTitle": "视频", "settingsVideoShowVideos": "显示视频", "settingsVideoEnableHardwareAcceleration": "硬件加速", - "settingsVideoEnableAutoPlay": "自动播放", + "settingsVideoAutoPlay": "自动播放", "settingsVideoLoopModeTile": "循环模式", "settingsVideoLoopModeDialogTitle": "循环模式", @@ -557,7 +567,6 @@ "settingsVideoControlsTile": "控件", "settingsVideoControlsPageTitle": "控件", "settingsVideoButtonsTile": "按钮", - "settingsVideoButtonsDialogTitle": "按钮", "settingsVideoGestureDoubleTapTogglePlay": "双击播放/暂停", "settingsVideoGestureSideDoubleTapSeek": "双击屏幕边缘步进/步退", @@ -590,7 +599,6 @@ "settingsRemoveAnimationsTile": "移除动画", "settingsRemoveAnimationsDialogTitle": "移除动画", "settingsTimeToTakeActionTile": "生效时间", - "settingsTimeToTakeActionDialogTitle": "生效时间", "settingsDisplaySectionTitle": "显示", "settingsThemeBrightnessTile": "主题", @@ -612,6 +620,7 @@ "settingsWidgetPageTitle": "相框", "settingsWidgetShowOutline": "轮廓", + "settingsWidgetOpenPage": "轻触小部件时", "settingsCollectionTile": "媒体集", @@ -620,6 +629,7 @@ "statsTopCountriesSectionTitle": "热门国家", "statsTopPlacesSectionTitle": "热门地点", "statsTopTagsSectionTitle": "热门标签", + "statsTopAlbumsSectionTitle": "热门相册", "viewerOpenPanoramaButtonLabel": "打开全景", "viewerSetWallpaperButtonLabel": "设置壁纸", @@ -664,6 +674,8 @@ "viewerInfoSearchSuggestionResolution": "分辨率", "viewerInfoSearchSuggestionRights": "所有权", + "wallpaperUseScrollEffect": "在主屏幕上使用滚动效果", + "tagEditorPageTitle": "编辑标签", "tagEditorPageNewTagFieldLabel": "新标签", "tagEditorPageAddTagTooltip": "添加标签", diff --git a/lib/main_common.dart b/lib/main_common.dart index 775607d8b..a02f671be 100644 --- a/lib/main_common.dart +++ b/lib/main_common.dart @@ -33,6 +33,7 @@ void mainCommon(AppFlavor flavor) { // - in profile/release mode: plain grey background // This can be modified via `ErrorWidget.builder` // ErrorWidget.builder = (details) => ErrorWidget(details.exception); + // cf https://docs.flutter.dev/testing/errors runApp(AvesApp(flavor: flavor)); } diff --git a/lib/model/actions/chip_actions.dart b/lib/model/actions/chip_actions.dart index efae273d7..28b596579 100644 --- a/lib/model/actions/chip_actions.dart +++ b/lib/model/actions/chip_actions.dart @@ -6,6 +6,7 @@ enum ChipAction { goToAlbumPage, goToCountryPage, goToTagPage, + reverse, hide, } @@ -18,6 +19,9 @@ extension ExtraChipAction on ChipAction { return context.l10n.chipActionGoToCountryPage; case ChipAction.goToTagPage: return context.l10n.chipActionGoToTagPage; + case ChipAction.reverse: + // different data depending on state + return context.l10n.chipActionFilterOut; case ChipAction.hide: return context.l10n.chipActionHide; } @@ -33,6 +37,8 @@ extension ExtraChipAction on ChipAction { return AIcons.location; case ChipAction.goToTagPage: return AIcons.tag; + case ChipAction.reverse: + return AIcons.reverse; case ChipAction.hide: return AIcons.hide; } diff --git a/lib/model/actions/entry_set_actions.dart b/lib/model/actions/entry_set_actions.dart index 0efb48962..92af82d5c 100644 --- a/lib/model/actions/entry_set_actions.dart +++ b/lib/model/actions/entry_set_actions.dart @@ -83,7 +83,7 @@ class EntrySetActions { ]; // exclude bin related actions - static const collectionEditorSelection = [ + static const collectionEditorSelectionRegular = [ EntrySetAction.share, EntrySetAction.delete, EntrySetAction.copy, @@ -97,6 +97,18 @@ class EntrySetActions { // editing actions are in their subsection ]; + static const collectionEditorSelectionEdit = [ + EntrySetAction.rotateCCW, + EntrySetAction.rotateCW, + EntrySetAction.flip, + EntrySetAction.editDate, + EntrySetAction.editLocation, + EntrySetAction.editTitleDescription, + EntrySetAction.editRating, + EntrySetAction.editTags, + EntrySetAction.removeMetadata, + ]; + static const edit = [ EntrySetAction.editDate, EntrySetAction.editLocation, diff --git a/lib/model/db/db_metadata.dart b/lib/model/db/db_metadata.dart index 7f5d70ea4..cc23707bf 100644 --- a/lib/model/db/db_metadata.dart +++ b/lib/model/db/db_metadata.dart @@ -10,8 +10,6 @@ import 'package:aves/model/video_playback.dart'; abstract class MetadataDb { int get nextId; - int get timestampSecs; - Future init(); Future dbFileSize(); diff --git a/lib/model/db/db_metadata_sqflite.dart b/lib/model/db/db_metadata_sqflite.dart index 8ad1aa3fe..3e12a018a 100644 --- a/lib/model/db/db_metadata_sqflite.dart +++ b/lib/model/db/db_metadata_sqflite.dart @@ -34,9 +34,6 @@ class SqfliteMetadataDb implements MetadataDb { @override int get nextId => ++_lastId; - @override - int get timestampSecs => DateTime.now().millisecondsSinceEpoch ~/ 1000; - @override Future init() async { _db = await openDatabase( diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 65443958b..a057462b0 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -26,7 +26,7 @@ import 'package:country_code/country_code.dart'; import 'package:flutter/foundation.dart'; import 'package:latlong2/latlong.dart'; -enum EntryDataType { basic, catalog, address, references } +enum EntryDataType { basic, aspectRatio, catalog, address, references } class AvesEntry { // `sizeBytes`, `dateModifiedSecs` can be missing in viewer mode @@ -150,6 +150,7 @@ class AvesEntry { 'sourceRotationDegrees': sourceRotationDegrees, 'sizeBytes': sizeBytes, 'title': sourceTitle, + 'dateAddedSecs': dateAddedSecs, 'dateModifiedSecs': dateModifiedSecs, 'sourceDateTakenMillis': sourceDateTakenMillis, 'durationMillis': durationMillis, @@ -277,6 +278,8 @@ class AvesEntry { bool get isMediaStoreContent => uri.startsWith('content://media/'); + bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains); + bool get canEdit => path != null && !trashed && isMediaStoreContent; bool get canEditDate => canEdit && (canEditExif || canEditXmp); @@ -291,9 +294,9 @@ class AvesEntry { bool get canRotateAndFlip => canEdit && canEditExif; - // as of androidx.exifinterface:exifinterface:1.3.3 - // `exifinterface` declares support for DNG, but `exifinterface` strips non-standard Exif tags when saving attributes, - // and DNG requires DNG-specific tags saved along standard Exif. So `exifinterface` actually breaks DNG files. + // `exifinterface` v1.3.3 declared support for DNG, but it strips non-standard Exif tags when saving attributes, + // and DNG requires DNG-specific tags saved along standard Exif. So it was actually breaking DNG files. + // as of androidx.exifinterface:exifinterface:1.3.4 bool get canEditExif { switch (mimeType.toLowerCase()) { case MimeTypes.jpeg: diff --git a/lib/model/entry_metadata_edition.dart b/lib/model/entry_metadata_edition.dart index 1659b20bb..7692ef761 100644 --- a/lib/model/entry_metadata_edition.dart +++ b/lib/model/entry_metadata_edition.dart @@ -126,6 +126,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { if (newFields.isNotEmpty) { dataTypes.addAll({ EntryDataType.basic, + EntryDataType.aspectRatio, EntryDataType.catalog, }); } @@ -309,14 +310,18 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { } Future> removeMetadata(Set types) async { + final Set dataTypes = {}; + final newFields = await metadataEditService.removeTypes(this, types); - return newFields.isEmpty - ? {} - : { - EntryDataType.basic, - EntryDataType.catalog, - EntryDataType.address, - }; + if (newFields.isNotEmpty) { + dataTypes.addAll({ + EntryDataType.basic, + EntryDataType.aspectRatio, + EntryDataType.catalog, + EntryDataType.address, + }); + } + return dataTypes; } static void editIptcValues(List> iptc, int record, int tag, Set values) { diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index 431f52e18..f4b01222f 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -14,16 +14,20 @@ class AlbumFilter extends CoveredCollectionFilter { final String album; final String? displayName; + late final EntryFilter _test; @override - List get props => [album]; + List get props => [album, reversed]; - const AlbumFilter(this.album, this.displayName); + AlbumFilter(this.album, this.displayName, {super.reversed = false}) { + _test = (entry) => entry.directory == album; + } factory AlbumFilter.fromMap(Map json) { return AlbumFilter( json['album'], json['uniqueName'], + reversed: json['reversed'] ?? false, ); } @@ -32,10 +36,14 @@ class AlbumFilter extends CoveredCollectionFilter { 'type': type, 'album': album, 'uniqueName': displayName, + 'reversed': reversed, }; @override - EntryFilter get test => (entry) => entry.directory == album; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => true; @override String get universalLabel => displayName ?? pContext.split(album).last; diff --git a/lib/model/filters/coordinate.dart b/lib/model/filters/coordinate.dart index f08edc62e..20dff71da 100644 --- a/lib/model/filters/coordinate.dart +++ b/lib/model/filters/coordinate.dart @@ -17,16 +17,20 @@ class CoordinateFilter extends CollectionFilter { final LatLng sw; final LatLng ne; final bool minuteSecondPadding; + late final EntryFilter _test; @override - List get props => [sw, ne]; + List get props => [sw, ne, reversed]; - const CoordinateFilter(this.sw, this.ne, {this.minuteSecondPadding = false}); + CoordinateFilter(this.sw, this.ne, {this.minuteSecondPadding = false, super.reversed = false}) { + _test = (entry) => GeoUtils.contains(sw, ne, entry.latLng); + } factory CoordinateFilter.fromMap(Map json) { return CoordinateFilter( LatLng.fromJson(json['sw']), LatLng.fromJson(json['ne']), + reversed: json['reversed'] ?? false, ); } @@ -35,10 +39,11 @@ class CoordinateFilter extends CollectionFilter { 'type': type, 'sw': sw.toJson(), 'ne': ne.toJson(), + 'reversed': reversed, }; @override - EntryFilter get test => (entry) => GeoUtils.contains(sw, ne, entry.latLng); + EntryFilter get positiveTest => _test; String _formatBounds(AppLocalizations l10n, CoordinateFormat format) { String s(LatLng latLng) => format.format( @@ -50,6 +55,9 @@ class CoordinateFilter extends CollectionFilter { return '${s(ne)}\n${s(sw)}'; } + @override + bool get exclusiveProp => false; + @override String get universalLabel => _formatBounds(lookupAppLocalizations(AppLocalizations.supportedLocales.first), CoordinateFormat.decimal); diff --git a/lib/model/filters/date.dart b/lib/model/filters/date.dart index 6262bd8c5..c56876881 100644 --- a/lib/model/filters/date.dart +++ b/lib/model/filters/date.dart @@ -18,9 +18,9 @@ class DateFilter extends CollectionFilter { static final onThisDay = DateFilter(DateLevel.md, null); @override - List get props => [level, date]; + List get props => [level, date, reversed]; - DateFilter(this.level, this.date) { + DateFilter(this.level, this.date, {super.reversed = false}) { _effectiveDate = date ?? DateTime.now(); switch (level) { case DateLevel.y: @@ -56,6 +56,7 @@ class DateFilter extends CollectionFilter { return DateFilter( DateLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? DateLevel.ymd, dateString != null ? DateTime.tryParse(dateString) : null, + reversed: json['reversed'] ?? false, ); } @@ -64,15 +65,20 @@ class DateFilter extends CollectionFilter { 'type': type, 'level': level.toString(), 'date': date?.toIso8601String(), + 'reversed': reversed, }; @override - EntryFilter get test => _test; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => true; @override bool isCompatible(CollectionFilter other) { if (other is DateFilter) { - return isCompatibleLevel(level, other.level); + if (reversed != other.reversed && this == other.reverse()) return false; + return reversed || other.reversed || isCompatibleLevel(level, other.level); } else { return true; } diff --git a/lib/model/filters/favourite.dart b/lib/model/filters/favourite.dart index 35ee91b65..197fb21d1 100644 --- a/lib/model/filters/favourite.dart +++ b/lib/model/filters/favourite.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; @@ -9,20 +10,32 @@ import 'package:provider/provider.dart'; class FavouriteFilter extends CollectionFilter { static const type = 'favourite'; + static bool _test(AvesEntry entry) => entry.isFavourite; + static const instance = FavouriteFilter._private(); + static const instanceReversed = FavouriteFilter._private(reversed: true); @override - List get props => []; + List get props => [reversed]; - const FavouriteFilter._private(); + const FavouriteFilter._private({super.reversed = false}); + + factory FavouriteFilter.fromMap(Map json) { + final reversed = json['reversed'] ?? false; + return reversed ? instanceReversed : instance; + } @override Map toMap() => { 'type': type, + 'reversed': reversed, }; @override - EntryFilter get test => (entry) => entry.isFavourite; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => false; @override String get universalLabel => type; diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index a57b0cce7..930dd2da2 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -42,9 +42,44 @@ abstract class CollectionFilter extends Equatable implements Comparable jsonMap) { + final type = jsonMap['type']; + switch (type) { + case AlbumFilter.type: + return AlbumFilter.fromMap(jsonMap); + case CoordinateFilter.type: + return CoordinateFilter.fromMap(jsonMap); + case DateFilter.type: + return DateFilter.fromMap(jsonMap); + case FavouriteFilter.type: + return FavouriteFilter.fromMap(jsonMap); + case LocationFilter.type: + return LocationFilter.fromMap(jsonMap); + case MimeFilter.type: + return MimeFilter.fromMap(jsonMap); + case MissingFilter.type: + return MissingFilter.fromMap(jsonMap); + case PathFilter.type: + return PathFilter.fromMap(jsonMap); + case QueryFilter.type: + return QueryFilter.fromMap(jsonMap); + case RatingFilter.type: + return RatingFilter.fromMap(jsonMap); + case RecentlyAddedFilter.type: + return RecentlyAddedFilter.fromMap(jsonMap); + case TagFilter.type: + return TagFilter.fromMap(jsonMap); + case TypeFilter.type: + return TypeFilter.fromMap(jsonMap); + case TrashFilter.type: + return TrashFilter.fromMap(jsonMap); + } + return null; + } static CollectionFilter? fromJson(String jsonString) { if (jsonString.isEmpty) return null; @@ -52,37 +87,7 @@ abstract class CollectionFilter extends Equatable implements Comparable) { - final type = jsonMap['type']; - switch (type) { - case AlbumFilter.type: - return AlbumFilter.fromMap(jsonMap); - case CoordinateFilter.type: - return CoordinateFilter.fromMap(jsonMap); - case DateFilter.type: - return DateFilter.fromMap(jsonMap); - case FavouriteFilter.type: - return FavouriteFilter.instance; - case LocationFilter.type: - return LocationFilter.fromMap(jsonMap); - case MimeFilter.type: - return MimeFilter.fromMap(jsonMap); - case MissingFilter.type: - return MissingFilter.fromMap(jsonMap); - case PathFilter.type: - return PathFilter.fromMap(jsonMap); - case QueryFilter.type: - return QueryFilter.fromMap(jsonMap); - case RatingFilter.type: - return RatingFilter.fromMap(jsonMap); - case RecentlyAddedFilter.type: - return RecentlyAddedFilter.instance; - case TagFilter.type: - return TagFilter.fromMap(jsonMap); - case TypeFilter.type: - return TypeFilter.fromMap(jsonMap); - case TrashFilter.type: - return TrashFilter.instance; - } + return _fromMap(jsonMap); } } catch (error, stack) { debugPrint('failed to parse filter from json=$jsonString error=$error\n$stack'); @@ -95,9 +100,21 @@ abstract class CollectionFilter extends Equatable implements Comparable jsonEncode(toMap()); - EntryFilter get test; + EntryFilter get positiveTest; - bool isCompatible(CollectionFilter other) => category != other.category; + EntryFilter get test => reversed ? (v) => !positiveTest(v) : positiveTest; + + CollectionFilter reverse() => _fromMap(toMap()..['reversed'] = !reversed)!; + + bool get exclusiveProp; + + bool isCompatible(CollectionFilter other) { + if (category != other.category) return true; + if (!reversed && !other.reversed) return !exclusiveProp; + if (reversed && other.reversed) return true; + if (this == other.reverse()) return false; + return true; + } String get universalLabel; @@ -129,7 +146,7 @@ abstract class CollectionFilter extends Equatable implements Comparable color(BuildContext context) { diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index 9b219c806..3fad0a7a3 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -15,9 +15,9 @@ class LocationFilter extends CoveredCollectionFilter { late final EntryFilter _test; @override - List get props => [level, _location, _countryCode]; + List get props => [level, _location, _countryCode, reversed]; - LocationFilter(this.level, String location) { + LocationFilter(this.level, String location, {super.reversed = false}) { final split = location.split(locationSeparator); _location = split.isNotEmpty ? split[0] : location; _countryCode = split.length > 1 ? split[1] : null; @@ -35,6 +35,7 @@ class LocationFilter extends CoveredCollectionFilter { return LocationFilter( LocationLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? LocationLevel.place, json['location'], + reversed: json['reversed'] ?? false, ); } @@ -43,6 +44,7 @@ class LocationFilter extends CoveredCollectionFilter { 'type': type, 'level': level.toString(), 'location': _countryCode != null ? countryNameAndCode : _location, + 'reversed': reversed, }; String get countryNameAndCode => '$_location$locationSeparator$_countryCode'; @@ -50,7 +52,10 @@ class LocationFilter extends CoveredCollectionFilter { String? get countryCode => _countryCode; @override - EntryFilter get test => _test; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => true; @override String get universalLabel => _location; diff --git a/lib/model/filters/mime.dart b/lib/model/filters/mime.dart index 98fb3f9de..bceaadc09 100644 --- a/lib/model/filters/mime.dart +++ b/lib/model/filters/mime.dart @@ -14,17 +14,17 @@ class MimeFilter extends CollectionFilter { static const type = 'mime'; final String mime; - late final EntryFilter _test; late final String _label; late final IconData _icon; + late final EntryFilter _test; static final image = MimeFilter(MimeTypes.anyImage); static final video = MimeFilter(MimeTypes.anyVideo); @override - List get props => [mime]; + List get props => [mime, reversed]; - MimeFilter(this.mime) { + MimeFilter(this.mime, {super.reversed = false}) { IconData? icon; var lowMime = mime.toLowerCase(); if (lowMime.endsWith('/*')) { @@ -46,6 +46,7 @@ class MimeFilter extends CollectionFilter { factory MimeFilter.fromMap(Map json) { return MimeFilter( json['mime'], + reversed: json['reversed'] ?? false, ); } @@ -53,10 +54,14 @@ class MimeFilter extends CollectionFilter { Map toMap() => { 'type': type, 'mime': mime, + 'reversed': reversed, }; @override - EntryFilter get test => _test; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => true; @override String get universalLabel => _label; diff --git a/lib/model/filters/missing.dart b/lib/model/filters/missing.dart index 119355e3e..03c80646d 100644 --- a/lib/model/filters/missing.dart +++ b/lib/model/filters/missing.dart @@ -10,16 +10,16 @@ class MissingFilter extends CollectionFilter { static const _title = 'title'; final String metadataType; - late final EntryFilter _test; late final IconData _icon; + late final EntryFilter _test; static final date = MissingFilter._private(_date); static final title = MissingFilter._private(_title); @override - List get props => [metadataType]; + List get props => [metadataType, reversed]; - MissingFilter._private(this.metadataType) { + MissingFilter._private(this.metadataType, {super.reversed = false}) { switch (metadataType) { case _date: _test = (entry) => (entry.catalogMetadata?.dateMillis ?? 0) == 0; @@ -35,6 +35,7 @@ class MissingFilter extends CollectionFilter { factory MissingFilter.fromMap(Map json) { return MissingFilter._private( json['metadataType'], + reversed: json['reversed'] ?? false, ); } @@ -42,10 +43,14 @@ class MissingFilter extends CollectionFilter { Map toMap() => { 'type': type, 'metadataType': metadataType, + 'reversed': reversed, }; @override - EntryFilter get test => _test; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => false; @override String get universalLabel => metadataType; diff --git a/lib/model/filters/path.dart b/lib/model/filters/path.dart index ce1c1ead8..cb5d5308a 100644 --- a/lib/model/filters/path.dart +++ b/lib/model/filters/path.dart @@ -10,14 +10,24 @@ class PathFilter extends CollectionFilter { // without trailing separator final String _rootAlbum; - @override - List get props => [path]; + late final EntryFilter _test; - PathFilter(this.path) : _rootAlbum = path.substring(0, path.length - 1); + @override + List get props => [path, reversed]; + + PathFilter(this.path, {super.reversed = false}) : _rootAlbum = path.substring(0, path.length - 1) { + _test = (entry) { + final dir = entry.directory; + if (dir == null) return false; + // avoid string building in most cases + return dir.startsWith(_rootAlbum) && '$dir${pContext.separator}'.startsWith(path); + }; + } factory PathFilter.fromMap(Map json) { return PathFilter( json['path'], + reversed: json['reversed'] ?? false, ); } @@ -25,15 +35,14 @@ class PathFilter extends CollectionFilter { Map toMap() => { 'type': type, 'path': path, + 'reversed': reversed, }; @override - EntryFilter get test => (entry) { - final dir = entry.directory; - if (dir == null) return false; - // avoid string building in most cases - return dir.startsWith(_rootAlbum) && '$dir${pContext.separator}'.startsWith(path); - }; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => true; @override String get universalLabel => path; diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index 7785b0345..05ccc1c6d 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -18,7 +18,7 @@ class QueryFilter extends CollectionFilter { late final EntryFilter _test; @override - List get props => [query, live]; + List get props => [query, live, reversed]; static final _fieldPattern = RegExp(r'(.+)([=<>])(.+)'); static final _fileSizePattern = RegExp(r'(\d+)([KMG])?'); @@ -33,7 +33,7 @@ class QueryFilter extends CollectionFilter { static const opLower = '<'; static const opGreater = '>'; - QueryFilter(this.query, {this.colorful = true, this.live = false}) { + QueryFilter(this.query, {this.colorful = true, this.live = false, super.reversed = false}) { var upQuery = query.toUpperCase(); final test = fieldTest(upQuery); @@ -62,6 +62,7 @@ class QueryFilter extends CollectionFilter { factory QueryFilter.fromMap(Map json) { return QueryFilter( json['query'], + reversed: json['reversed'] ?? false, ); } @@ -69,13 +70,14 @@ class QueryFilter extends CollectionFilter { Map toMap() => { 'type': type, 'query': query, + 'reversed': reversed, }; @override - EntryFilter get test => _test; + EntryFilter get positiveTest => _test; @override - bool isCompatible(CollectionFilter other) => true; + bool get exclusiveProp => false; @override String get universalLabel => query; diff --git a/lib/model/filters/rating.dart b/lib/model/filters/rating.dart index ad516aa5e..ad2a44998 100644 --- a/lib/model/filters/rating.dart +++ b/lib/model/filters/rating.dart @@ -7,15 +7,19 @@ class RatingFilter extends CollectionFilter { static const type = 'rating'; final int rating; + late final EntryFilter _test; @override - List get props => [rating]; + List get props => [rating, reversed]; - const RatingFilter(this.rating); + RatingFilter(this.rating, {super.reversed = false}) { + _test = (entry) => entry.rating == rating; + } factory RatingFilter.fromMap(Map json) { return RatingFilter( json['rating'] ?? 0, + reversed: json['reversed'] ?? false, ); } @@ -23,10 +27,14 @@ class RatingFilter extends CollectionFilter { Map toMap() => { 'type': type, 'rating': rating, + 'reversed': reversed, }; @override - EntryFilter get test => (entry) => entry.rating == rating; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => true; @override String get universalLabel => '$rating'; diff --git a/lib/model/filters/recent.dart b/lib/model/filters/recent.dart index f6ffb1a0b..c1555bab0 100644 --- a/lib/model/filters/recent.dart +++ b/lib/model/filters/recent.dart @@ -6,30 +6,43 @@ import 'package:flutter/material.dart'; class RecentlyAddedFilter extends CollectionFilter { static const type = 'recently_added'; + static late EntryFilter _test; + static final instance = RecentlyAddedFilter._private(); + static final instanceReversed = RecentlyAddedFilter._private(reversed: true); static late int nowSecs; static void updateNow() { nowSecs = DateTime.now().millisecondsSinceEpoch ~/ 1000; + _test = (entry) => (nowSecs - (entry.dateAddedSecs ?? 0)) < _dayInSecs; } static const _dayInSecs = 24 * 60 * 60; @override - List get props => []; + List get props => [reversed]; - RecentlyAddedFilter._private() { + RecentlyAddedFilter._private({super.reversed = false}) { updateNow(); } + factory RecentlyAddedFilter.fromMap(Map json) { + final reversed = json['reversed'] ?? false; + return reversed ? instanceReversed : instance; + } + @override Map toMap() => { 'type': type, + 'reversed': reversed, }; @override - EntryFilter get test => (entry) => (nowSecs - (entry.dateAddedSecs ?? 0)) < _dayInSecs; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => false; @override String get universalLabel => type; diff --git a/lib/model/filters/tag.dart b/lib/model/filters/tag.dart index b72624da1..389da1f4e 100644 --- a/lib/model/filters/tag.dart +++ b/lib/model/filters/tag.dart @@ -10,20 +10,20 @@ class TagFilter extends CoveredCollectionFilter { late final EntryFilter _test; @override - List get props => [tag]; + List get props => [tag, reversed]; - TagFilter(this.tag, {bool not = false}) : super(not: not) { + TagFilter(this.tag, {super.reversed = false}) { if (tag.isEmpty) { - _test = not ? (entry) => entry.tags.isNotEmpty : (entry) => entry.tags.isEmpty; + _test = (entry) => entry.tags.isEmpty; } else { - _test = not ? (entry) => !entry.tags.contains(tag) : (entry) => entry.tags.contains(tag); + _test = (entry) => entry.tags.contains(tag); } } factory TagFilter.fromMap(Map json) { return TagFilter( json['tag'], - not: json['not'] ?? false, + reversed: json['reversed'] ?? false, ); } @@ -31,14 +31,14 @@ class TagFilter extends CoveredCollectionFilter { Map toMap() => { 'type': type, 'tag': tag, - 'not': not, + 'reversed': reversed, }; @override - EntryFilter get test => _test; + EntryFilter get positiveTest => _test; @override - bool isCompatible(CollectionFilter other) => true; + bool get exclusiveProp => false; @override String get universalLabel => tag; diff --git a/lib/model/filters/trash.dart b/lib/model/filters/trash.dart index 536318fa7..666236dc2 100644 --- a/lib/model/filters/trash.dart +++ b/lib/model/filters/trash.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -6,20 +7,32 @@ import 'package:flutter/material.dart'; class TrashFilter extends CollectionFilter { static const type = 'trash'; + static bool _test(AvesEntry entry) => entry.trashed; + static const instance = TrashFilter._private(); + static const instanceReversed = TrashFilter._private(reversed: true); @override - List get props => []; + List get props => [reversed]; - const TrashFilter._private(); + const TrashFilter._private({super.reversed = false}); + + factory TrashFilter.fromMap(Map json) { + final reversed = json['reversed'] ?? false; + return reversed ? instanceReversed : instance; + } @override Map toMap() => { 'type': type, + 'reversed': reversed, }; @override - EntryFilter get test => (entry) => entry.trashed; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => false; @override String get universalLabel => type; diff --git a/lib/model/filters/type.dart b/lib/model/filters/type.dart index bef26c27b..7a7788466 100644 --- a/lib/model/filters/type.dart +++ b/lib/model/filters/type.dart @@ -17,8 +17,8 @@ class TypeFilter extends CollectionFilter { static const _sphericalVideo = 'spherical_video'; // subset of videos final String itemType; - late final EntryFilter _test; late final IconData _icon; + late final EntryFilter _test; static final animated = TypeFilter._private(_animated); static final geotiff = TypeFilter._private(_geotiff); @@ -28,9 +28,9 @@ class TypeFilter extends CollectionFilter { static final sphericalVideo = TypeFilter._private(_sphericalVideo); @override - List get props => [itemType]; + List get props => [itemType, reversed]; - TypeFilter._private(this.itemType) { + TypeFilter._private(this.itemType, {super.reversed = false}) { switch (itemType) { case _animated: _test = (entry) => entry.isAnimated; @@ -62,6 +62,7 @@ class TypeFilter extends CollectionFilter { factory TypeFilter.fromMap(Map json) { return TypeFilter._private( json['itemType'], + reversed: json['reversed'] ?? false, ); } @@ -69,10 +70,14 @@ class TypeFilter extends CollectionFilter { Map toMap() => { 'type': type, 'itemType': itemType, + 'reversed': reversed, }; @override - EntryFilter get test => _test; + EntryFilter get positiveTest => _test; + + @override + bool get exclusiveProp => false; @override String get universalLabel => itemType; diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index 4e21d4ba8..08b79553b 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -1,7 +1,5 @@ import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_set_actions.dart'; -import 'package:aves/model/filters/favourite.dart'; -import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/naming_pattern.dart'; import 'package:aves/model/settings/enums/enums.dart'; @@ -40,8 +38,6 @@ class SettingsDefaults { static const setMetadataDateBeforeFileOp = false; static final drawerTypeBookmarks = [ null, - MimeFilter.video, - FavouriteFilter.instance, RecentlyAddedFilter.instance, ]; static const drawerPageBookmarks = [ @@ -93,7 +89,7 @@ class SettingsDefaults { // video static const enableVideoHardwareAcceleration = true; - static const enableVideoAutoPlay = false; + static const videoAutoPlayMode = VideoAutoPlayMode.disabled; static const videoLoopMode = VideoLoopMode.shortOnly; static const videoShowRawTimedText = false; static const videoControls = VideoControls.play; @@ -133,6 +129,7 @@ class SettingsDefaults { static const slideshowRepeat = false; static const slideshowShuffle = false; static const slideshowFillScreen = false; + static const slideshowAnimatedZoomEffect = true; static const slideshowTransition = ViewerTransition.fade; static const slideshowVideoPlayback = SlideshowVideoPlayback.playMuted; static const slideshowInterval = SlideshowInterval.s5; @@ -140,6 +137,7 @@ class SettingsDefaults { // widget static const widgetOutline = false; static const widgetShape = WidgetShape.rrect; + static const widgetOpenPage = WidgetOpenPage.viewer; // platform settings static const isRotationLocked = false; diff --git a/lib/model/settings/enums/enums.dart b/lib/model/settings/enums/enums.dart index 05f34a63e..79f4cc1e7 100644 --- a/lib/model/settings/enums/enums.dart +++ b/lib/model/settings/enums/enums.dart @@ -28,6 +28,10 @@ enum VideoControls { play, playSeek, playOutside, none } enum VideoLoopMode { never, shortOnly, always } -enum ViewerTransition { slide, parallax, fade, zoomIn } +enum VideoAutoPlayMode { disabled, playMuted, playWithSound } -enum WidgetShape { rrect, circle, heart } \ No newline at end of file +enum ViewerTransition { slide, parallax, fade, zoomIn, none } + +enum WidgetOpenPage { home, viewer } + +enum WidgetShape { rrect, circle, heart } diff --git a/lib/model/settings/enums/slideshow_video_playback.dart b/lib/model/settings/enums/slideshow_video_playback.dart index beda9a052..453ddf29b 100644 --- a/lib/model/settings/enums/slideshow_video_playback.dart +++ b/lib/model/settings/enums/slideshow_video_playback.dart @@ -7,11 +7,11 @@ extension ExtraSlideshowVideoPlayback on SlideshowVideoPlayback { String getName(BuildContext context) { switch (this) { case SlideshowVideoPlayback.skip: - return context.l10n.slideshowVideoPlaybackSkip; + return context.l10n.videoPlaybackSkip; case SlideshowVideoPlayback.playMuted: - return context.l10n.slideshowVideoPlaybackMuted; + return context.l10n.videoPlaybackMuted; case SlideshowVideoPlayback.playWithSound: - return context.l10n.slideshowVideoPlaybackWithSound; + return context.l10n.videoPlaybackWithSound; } } } diff --git a/lib/model/settings/enums/video_auto_play_mode.dart b/lib/model/settings/enums/video_auto_play_mode.dart new file mode 100644 index 000000000..9552cedcb --- /dev/null +++ b/lib/model/settings/enums/video_auto_play_mode.dart @@ -0,0 +1,17 @@ +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/widgets.dart'; + +import 'enums.dart'; + +extension ExtraSlideshowVideoPlayback on VideoAutoPlayMode { + String getName(BuildContext context) { + switch (this) { + case VideoAutoPlayMode.disabled: + return context.l10n.settingsDisabled; + case VideoAutoPlayMode.playMuted: + return context.l10n.videoPlaybackMuted; + case VideoAutoPlayMode.playWithSound: + return context.l10n.videoPlaybackWithSound; + } + } +} diff --git a/lib/model/settings/enums/viewer_transition.dart b/lib/model/settings/enums/viewer_transition.dart index 504474851..e5fe2032e 100644 --- a/lib/model/settings/enums/viewer_transition.dart +++ b/lib/model/settings/enums/viewer_transition.dart @@ -15,6 +15,8 @@ extension ExtraViewerTransition on ViewerTransition { return context.l10n.viewerTransitionFade; case ViewerTransition.zoomIn: return context.l10n.viewerTransitionZoomIn; + case ViewerTransition.none: + return context.l10n.viewerTransitionNone; } } @@ -28,6 +30,8 @@ extension ExtraViewerTransition on ViewerTransition { return PageTransitionEffects.fade(pageController, index, zoomIn: false); case ViewerTransition.zoomIn: return PageTransitionEffects.fade(pageController, index, zoomIn: true); + case ViewerTransition.none: + return PageTransitionEffects.none(pageController, index); } } } diff --git a/lib/model/settings/enums/widget_open_action.dart b/lib/model/settings/enums/widget_open_action.dart new file mode 100644 index 000000000..804b99b46 --- /dev/null +++ b/lib/model/settings/enums/widget_open_action.dart @@ -0,0 +1,14 @@ +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/widgets.dart'; + +extension ExtraWidgetOpenPage on WidgetOpenPage { + String getName(BuildContext context) { + switch (this) { + case WidgetOpenPage.home: + return context.l10n.widgetOpenPageHome; + case WidgetOpenPage.viewer: + return context.l10n.widgetOpenPageViewer; + } + } +} diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index fd21a2409..71a40dcca 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -112,7 +112,7 @@ class Settings extends ChangeNotifier { // video static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec'; - static const enableVideoAutoPlayKey = 'video_auto_play'; + static const videoAutoPlayModeKey = 'video_auto_play_mode'; static const videoLoopModeKey = 'video_loop'; static const videoShowRawTimedTextKey = 'video_show_raw_timed_text'; static const videoControlsKey = 'video_controls'; @@ -151,6 +151,7 @@ class Settings extends ChangeNotifier { // screen saver static const screenSaverFillScreenKey = 'screen_saver_fill_screen'; + static const screenSaverAnimatedZoomEffectKey = 'screen_saver_animated_zoom_effect'; static const screenSaverTransitionKey = 'screen_saver_transition'; static const screenSaverVideoPlaybackKey = 'screen_saver_video_playback'; static const screenSaverIntervalKey = 'screen_saver_interval'; @@ -160,6 +161,7 @@ class Settings extends ChangeNotifier { static const slideshowRepeatKey = 'slideshow_loop'; static const slideshowShuffleKey = 'slideshow_shuffle'; static const slideshowFillScreenKey = 'slideshow_fill_screen'; + static const slideshowAnimatedZoomEffectKey = 'slideshow_animated_zoom_effect'; static const slideshowTransitionKey = 'slideshow_transition'; static const slideshowVideoPlaybackKey = 'slideshow_video_playback'; static const slideshowIntervalKey = 'slideshow_interval'; @@ -168,6 +170,7 @@ class Settings extends ChangeNotifier { static const widgetOutlinePrefixKey = '${_widgetKeyPrefix}outline_'; static const widgetShapePrefixKey = '${_widgetKeyPrefix}shape_'; static const widgetCollectionFiltersPrefixKey = '${_widgetKeyPrefix}collection_filters_'; + static const widgetOpenPagePrefixKey = '${_widgetKeyPrefix}open_page_'; static const widgetUriPrefixKey = '${_widgetKeyPrefix}uri_'; // platform settings @@ -533,9 +536,9 @@ class Settings extends ChangeNotifier { set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue); - bool get enableVideoAutoPlay => getBoolOrDefault(enableVideoAutoPlayKey, SettingsDefaults.enableVideoAutoPlay); + VideoAutoPlayMode get videoAutoPlayMode => getEnumOrDefault(videoAutoPlayModeKey, SettingsDefaults.videoAutoPlayMode, VideoAutoPlayMode.values); - set enableVideoAutoPlay(bool newValue) => setAndNotify(enableVideoAutoPlayKey, newValue); + set videoAutoPlayMode(VideoAutoPlayMode newValue) => setAndNotify(videoAutoPlayModeKey, newValue.toString()); VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, SettingsDefaults.videoLoopMode, VideoLoopMode.values); @@ -648,6 +651,10 @@ class Settings extends ChangeNotifier { set screenSaverFillScreen(bool newValue) => setAndNotify(screenSaverFillScreenKey, newValue); + bool get screenSaverAnimatedZoomEffect => getBoolOrDefault(screenSaverAnimatedZoomEffectKey, SettingsDefaults.slideshowAnimatedZoomEffect); + + set screenSaverAnimatedZoomEffect(bool newValue) => setAndNotify(screenSaverAnimatedZoomEffectKey, newValue); + ViewerTransition get screenSaverTransition => getEnumOrDefault(screenSaverTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values); set screenSaverTransition(ViewerTransition newValue) => setAndNotify(screenSaverTransitionKey, newValue.toString()); @@ -678,6 +685,10 @@ class Settings extends ChangeNotifier { set slideshowFillScreen(bool newValue) => setAndNotify(slideshowFillScreenKey, newValue); + bool get slideshowAnimatedZoomEffect => getBoolOrDefault(slideshowAnimatedZoomEffectKey, SettingsDefaults.slideshowAnimatedZoomEffect); + + set slideshowAnimatedZoomEffect(bool newValue) => setAndNotify(slideshowAnimatedZoomEffectKey, newValue); + ViewerTransition get slideshowTransition => getEnumOrDefault(slideshowTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values); set slideshowTransition(ViewerTransition newValue) => setAndNotify(slideshowTransitionKey, newValue.toString()); @@ -707,6 +718,10 @@ class Settings extends ChangeNotifier { void setWidgetCollectionFilters(int widgetId, Set newValue) => setAndNotify('$widgetCollectionFiltersPrefixKey$widgetId', newValue.map((filter) => filter.toJson()).toList()); + WidgetOpenPage getWidgetOpenPage(int widgetId) => getEnumOrDefault('$widgetOpenPagePrefixKey$widgetId', SettingsDefaults.widgetOpenPage, WidgetOpenPage.values); + + void setWidgetOpenPage(int widgetId, WidgetOpenPage newValue) => setAndNotify('$widgetOpenPagePrefixKey$widgetId', newValue.toString()); + String? getWidgetUri(int widgetId) => getString('$widgetUriPrefixKey$widgetId'); void setWidgetUri(int widgetId, String? newValue) => setAndNotify('$widgetUriPrefixKey$widgetId', newValue); @@ -869,16 +884,17 @@ class Settings extends ChangeNotifier { case viewerMaxBrightnessKey: case enableMotionPhotoAutoPlayKey: case enableVideoHardwareAccelerationKey: - case enableVideoAutoPlayKey: case videoGestureDoubleTapTogglePlayKey: case videoGestureSideDoubleTapSeekKey: case subtitleShowOutlineKey: case saveSearchHistoryKey: case filePickerShowHiddenFilesKey: case screenSaverFillScreenKey: + case screenSaverAnimatedZoomEffectKey: case slideshowRepeatKey: case slideshowShuffleKey: case slideshowFillScreenKey: + case slideshowAnimatedZoomEffectKey: if (newValue is bool) { settingsStore.setBool(key, newValue); } else { @@ -898,6 +914,7 @@ class Settings extends ChangeNotifier { case countrySortFactorKey: case tagSortFactorKey: case imageBackgroundKey: + case videoAutoPlayModeKey: case videoLoopModeKey: case videoControlsKey: case subtitleTextAlignmentKey: diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index fbeec1e64..f4e48bdd8 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -128,7 +128,7 @@ class CollectionLens with ChangeNotifier { } bool get showHeaders { - bool showAlbumHeaders() => !filters.any((f) => f is AlbumFilter); + bool showAlbumHeaders() => !filters.any((v) => v is AlbumFilter && !v.reversed); switch (sortFactor) { case EntrySortFactor.date: diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 20b3a5063..94424e2b5 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -310,7 +310,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM contentId: newFields['contentId'] as int?, // title can change when moved files are automatically renamed to avoid conflict title: newFields['title'] as String?, - dateAddedSecs: metadataDb.timestampSecs, + dateAddedSecs: newFields['dateAddedSecs'] as int?, dateModifiedSecs: newFields['dateModifiedSecs'] as int?, )); } else { @@ -395,16 +395,25 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM Future refreshEntry(AvesEntry entry, Set dataTypes) async { await entry.refresh(background: false, persist: true, dataTypes: dataTypes, geocoderLocale: settings.appliedLocale); - // update/delete in DB final id = entry.id; - if (dataTypes.contains(EntryDataType.catalog)) { - await metadataDb.updateCatalogMetadata(id, entry.catalogMetadata); - onCatalogMetadataChanged(); - } - if (dataTypes.contains(EntryDataType.address)) { - await metadataDb.updateAddress(id, entry.addressDetails); - onAddressMetadataChanged(); - } + await Future.forEach(EntryDataType.values, (dataType) async { + switch (dataType) { + case EntryDataType.aspectRatio: + onAspectRatioChanged(); + break; + case EntryDataType.catalog: + await metadataDb.updateCatalogMetadata(id, entry.catalogMetadata); + onCatalogMetadataChanged(); + break; + case EntryDataType.address: + await metadataDb.updateAddress(id, entry.addressDetails); + onAddressMetadataChanged(); + break; + case EntryDataType.basic: + case EntryDataType.references: + break; + } + }); updateDerivedFilters({entry}); eventBus.fire(EntryRefreshedEvent({entry})); @@ -449,6 +458,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM state = SourceState.ready; } + void onAspectRatioChanged() => eventBus.fire(AspectRatioChangedEvent()); + // monitoring bool _monitoring = true; @@ -502,3 +513,5 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } } } + +class AspectRatioChangedEvent {} diff --git a/lib/model/source/enums/enums.dart b/lib/model/source/enums/enums.dart index ae59b22f3..de1b1faef 100644 --- a/lib/model/source/enums/enums.dart +++ b/lib/model/source/enums/enums.dart @@ -2,10 +2,10 @@ enum SourceState { loading, cataloguing, locatingCountries, locatingPlaces, read enum ChipSortFactor { date, name, count, size } -enum AlbumChipGroupFactor { none, importance, volume } +enum AlbumChipGroupFactor { none, importance, mimeType, volume } enum EntrySortFactor { date, name, rating, size } enum EntryGroupFactor { none, album, month, day } -enum TileLayout { grid, list } +enum TileLayout { mosaic, grid, list } diff --git a/lib/model/source/enums/view.dart b/lib/model/source/enums/view.dart index a80c116c9..aab4397f7 100644 --- a/lib/model/source/enums/view.dart +++ b/lib/model/source/enums/view.dart @@ -1,3 +1,4 @@ +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart'; @@ -18,6 +19,19 @@ extension ExtraEntrySortFactor on EntrySortFactor { } } + IconData get icon { + switch (this) { + case EntrySortFactor.date: + return AIcons.date; + case EntrySortFactor.name: + return AIcons.name; + case EntrySortFactor.rating: + return AIcons.rating; + case EntrySortFactor.size: + return AIcons.size; + } + } + String getOrderName(BuildContext context, bool reverse) { final l10n = context.l10n; switch (this) { @@ -48,6 +62,19 @@ extension ExtraChipSortFactor on ChipSortFactor { } } + IconData get icon { + switch (this) { + case ChipSortFactor.date: + return AIcons.date; + case ChipSortFactor.name: + return AIcons.name; + case ChipSortFactor.count: + return AIcons.count; + case ChipSortFactor.size: + return AIcons.size; + } + } + String getOrderName(BuildContext context, bool reverse) { final l10n = context.l10n; switch (this) { @@ -76,6 +103,19 @@ extension ExtraEntryGroupFactor on EntryGroupFactor { return l10n.collectionGroupNone; } } + + IconData get icon { + switch (this) { + case EntryGroupFactor.album: + return AIcons.album; + case EntryGroupFactor.month: + return AIcons.dateByMonth; + case EntryGroupFactor.day: + return AIcons.dateByDay; + case EntryGroupFactor.none: + return AIcons.clear; + } + } } extension ExtraAlbumChipGroupFactor on AlbumChipGroupFactor { @@ -84,22 +124,50 @@ extension ExtraAlbumChipGroupFactor on AlbumChipGroupFactor { switch (this) { case AlbumChipGroupFactor.importance: return l10n.albumGroupTier; + case AlbumChipGroupFactor.mimeType: + return l10n.albumGroupType; case AlbumChipGroupFactor.volume: return l10n.albumGroupVolume; case AlbumChipGroupFactor.none: return l10n.albumGroupNone; } } + + IconData get icon { + switch (this) { + case AlbumChipGroupFactor.importance: + return AIcons.important; + case AlbumChipGroupFactor.mimeType: + return AIcons.mimeType; + case AlbumChipGroupFactor.volume: + return AIcons.removableStorage; + case AlbumChipGroupFactor.none: + return AIcons.clear; + } + } } extension ExtraTileLayout on TileLayout { String getName(BuildContext context) { final l10n = context.l10n; switch (this) { + case TileLayout.mosaic: + return l10n.tileLayoutMosaic; case TileLayout.grid: return l10n.tileLayoutGrid; case TileLayout.list: return l10n.tileLayoutList; } } + + IconData get icon { + switch (this) { + case TileLayout.mosaic: + return AIcons.layoutMosaic; + case TileLayout.grid: + return AIcons.layoutGrid; + case TileLayout.list: + return AIcons.layoutList; + } + } } diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 18e6d4d44..d11cc5f1d 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -159,13 +159,7 @@ class MediaStoreSource extends CollectionSource { // reuse known entry ID to overwrite it while preserving favourites, etc. final contentId = entry.contentId; final existingEntry = knownContentIds.contains(contentId) ? knownLiveEntries.firstWhereOrNull((entry) => entry.contentId == contentId) : null; - if (existingEntry != null) { - entry.id = existingEntry.id; - entry.dateAddedSecs = existingEntry.dateAddedSecs; - } else { - entry.id = metadataDb.nextId; - entry.dateAddedSecs = metadataDb.timestampSecs; - } + entry.id = existingEntry?.id ?? metadataDb.nextId; pendingNewEntries.add(entry); if (pendingNewEntries.length >= refreshCount) { @@ -250,13 +244,7 @@ class MediaStoreSource extends CollectionSource { final newPath = sourceEntry.path; final volume = newPath != null ? androidFileUtils.getStorageVolume(newPath) : null; if (volume != null) { - if (existingEntry != null) { - sourceEntry.id = existingEntry.id; - sourceEntry.dateAddedSecs = existingEntry.dateAddedSecs; - } else { - sourceEntry.id = metadataDb.nextId; - sourceEntry.dateAddedSecs = metadataDb.timestampSecs; - } + sourceEntry.id = existingEntry?.id ?? metadataDb.nextId; newEntries.add(sourceEntry); final existingDirectory = existingEntry?.directory; if (existingDirectory != null) { diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index 451361e18..c050e6155 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -140,7 +140,7 @@ class VideoMetadataFormatter { hour = int.tryParse(match.group(5)!) ?? 0; minute = int.tryParse(match.group(6)!) ?? 0; second = int.tryParse(match.group(7)!) ?? 0; - pm = match.group(9) == 'pm'; + pm = {'pm', 'p. m.'}.contains(match.group(9)); } final date = DateTime(year, month, day, hour + (pm ? 12 : 0), minute, second, 0); diff --git a/lib/services/media/media_fetch_service.dart b/lib/services/media/media_fetch_service.dart index ad07eb85a..c3d13a55d 100644 --- a/lib/services/media/media_fetch_service.dart +++ b/lib/services/media/media_fetch_service.dart @@ -81,9 +81,8 @@ class PlatformMediaFetchService implements MediaFetchService { }) as Map; return AvesEntry.fromMap(result); } on PlatformException catch (e, stack) { - // do not report issues with simple parameter-less media content - // as it is likely an obsolete Media Store entry - if (!uri.startsWith('content://media/') || uri.contains('?')) { + // do not report issues with media content as it is likely an obsolete Media Store entry + if (!uri.startsWith('content://media/')) { await reportService.recordError(e, stack); } } diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index 50e81aab5..226f1b3b8 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -24,11 +24,12 @@ class Durations { static const chipDecorationAnimation = Duration(milliseconds: 200); static const highlightScrollAnimationMinMillis = 400; static const highlightScrollAnimationMaxMillis = 2000; + static const scalingGridBackgroundAnimation = Duration(milliseconds: 200); + static const scalingGridPositionAnimation = Duration(milliseconds: 150); // collection animations static const filterBarRemovalAnimation = Duration(milliseconds: 400); static const collectionOpOverlayAnimation = Duration(milliseconds: 300); - static const collectionScalingBackgroundAnimation = Duration(milliseconds: 200); static const sectionHeaderAnimation = Duration(milliseconds: 200); static const thumbnailOverlayAnimation = Duration(milliseconds: 200); @@ -40,6 +41,7 @@ class Durations { static const thumbnailScrollerShadeAnimation = Duration(milliseconds: 150); static const viewerVideoPlayerTransition = Duration(milliseconds: 500); static const viewerActionFeedbackAnimation = Duration(milliseconds: 600); + static const viewerHorizontalPageAnimation = Duration(seconds: 1); // info animations static const mapStyleSwitchAnimation = Duration(milliseconds: 300); @@ -95,6 +97,7 @@ class DurationsData { // common animations final Duration expansionTileAnimation; final Duration formTransition; + final Duration formTextStyleTransition; final Duration chartTransition; final Duration iconAnimation; final Duration staggeredAnimation; @@ -111,6 +114,7 @@ class DurationsData { const DurationsData({ this.expansionTileAnimation = const Duration(milliseconds: 200), this.formTransition = const Duration(milliseconds: 200), + this.formTextStyleTransition = const Duration(milliseconds: 800), this.chartTransition = const Duration(milliseconds: 400), this.iconAnimation = const Duration(milliseconds: 300), this.staggeredAnimation = const Duration(milliseconds: 375), @@ -125,6 +129,7 @@ class DurationsData { // as of Flutter v2.5.1, `ExpansionPanelList` throws if animation duration is zero expansionTileAnimation: const Duration(microseconds: 1), formTransition: Duration.zero, + formTextStyleTransition: Duration.zero, chartTransition: Duration.zero, iconAnimation: Duration.zero, staggeredAnimation: Duration.zero, diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 6dcdc8a03..7dd2853f1 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -14,8 +14,11 @@ class AIcons { static const IconData bin = Icons.delete_outlined; static const IconData broken = Icons.broken_image_outlined; static const IconData checked = Icons.done_outlined; + static const IconData count = MdiIcons.counter; static const IconData counter = Icons.plus_one_outlined; static const IconData date = Icons.calendar_today_outlined; + static const IconData dateByDay = Icons.today_outlined; + static const IconData dateByMonth = Icons.calendar_month_outlined; static const IconData dateRecent = Icons.today_outlined; static const IconData dateUndated = Icons.event_busy_outlined; static const IconData description = Icons.description_outlined; @@ -31,6 +34,7 @@ class AIcons { static const IconData location = Icons.place_outlined; static const IconData locationUnlocated = Icons.location_off_outlined; static const IconData mainStorage = Icons.smartphone_outlined; + static const IconData mimeType = Icons.code_outlined; static const IconData opacity = Icons.opacity; static const IconData privacy = MdiIcons.shieldAccountOutline; static const IconData rating = Icons.star_border_outlined; @@ -43,6 +47,7 @@ class AIcons { static const IconData sensorControlEnabled = Icons.explore_outlined; static const IconData sensorControlDisabled = Icons.explore_off_outlined; static const IconData settings = Icons.settings_outlined; + static const IconData size = Icons.data_usage_outlined; static const IconData text = Icons.format_quote_outlined; static const IconData tag = Icons.local_offer_outlined; static const IconData tagUntagged = MdiIcons.tagOffOutline; @@ -50,6 +55,9 @@ 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 layoutList = Icons.list_outlined; static const IconData sort = Icons.sort_outlined; static const IconData sortOrder = Icons.swap_vert_outlined; @@ -97,6 +105,7 @@ class AIcons { static const IconData print = Icons.print_outlined; static const IconData refresh = Icons.refresh_outlined; static const IconData replay10 = Icons.replay_10_outlined; + static const IconData reverse = Icons.invert_colors_outlined; static const IconData skip10 = Icons.forward_10_outlined; static const IconData reset = Icons.restart_alt_outlined; static const IconData restore = Icons.restore_outlined; @@ -111,7 +120,7 @@ class AIcons { static const IconData show = Icons.visibility_outlined; static const IconData slideshow = Icons.slideshow_outlined; static const IconData speed = Icons.speed_outlined; - static const IconData stats = Icons.pie_chart_outline_outlined; + static const IconData stats = Icons.donut_small_outlined; static const IconData streams = Icons.translate_outlined; static const IconData streamVideo = Icons.movie_outlined; static const IconData streamAudio = Icons.audiotrack_outlined; diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 89aa6b2f4..3d11b7beb 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -5,6 +5,11 @@ import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; class Constants { + // `Color(0x00FFFFFF)` is different from `Color(0x00000000)` (or `Colors.transparent`) + // when used in gradients or lerping to it + static const transparentWhite = Color(0x00FFFFFF); + static const transparentBlack = Colors.transparent; + // as of Flutter v2.8.0, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped // so we give it a `strutStyle` with a slightly larger height static const overflowStrutStyle = StrutStyle(height: 1.3); @@ -307,6 +312,11 @@ class Constants { license: 'MIT', sourceUrl: 'https://github.com/rrousselGit/provider', ), + Dependency( + name: 'Smooth Page Indicator', + license: 'MIT', + sourceUrl: 'https://github.com/Milad-Akarie/smooth_page_indicator', + ), ]; static const List dartPackages = [ diff --git a/lib/utils/xmp_utils.dart b/lib/utils/xmp_utils.dart index 49860cda7..f594ea82b 100644 --- a/lib/utils/xmp_utils.dart +++ b/lib/utils/xmp_utils.dart @@ -10,6 +10,7 @@ class Namespaces { static const container = 'http://ns.google.com/photos/1.0/container/'; static const creatorAtom = 'http://ns.adobe.com/creatorAtom/1.0/'; static const crd = 'http://ns.adobe.com/camera-raw-defaults/1.0/'; + static const crlcp = 'http://ns.adobe.com/camera-raw-embedded-lens-profile/1.0/'; static const crs = 'http://ns.adobe.com/camera-raw-settings/1.0/'; static const crss = 'http://ns.adobe.com/camera-raw-saved-settings/1.0/'; static const darktable = 'http://darktable.sf.net/'; @@ -30,7 +31,8 @@ class Namespaces { static const gettyImagesGift = 'http://xmp.gettyimages.com/gift/1.0/'; static const gFocus = 'http://ns.google.com/photos/1.0/focus/'; static const gImage = 'http://ns.google.com/photos/1.0/image/'; - static const gimp = 'http://www.gimp.org/ns/2.10/'; + static const gimp210 = 'http://www.gimp.org/ns/2.10/'; + static const gimpXmp = 'http://www.gimp.org/xmp/'; static const gPano = 'http://ns.google.com/photos/1.0/panorama/'; static const gSpherical = 'http://ns.google.com/videos/1.0/spherical/'; static const illustrator = 'http://ns.adobe.com/illustrator/1.0/'; @@ -56,6 +58,7 @@ class Namespaces { static const plus = 'http://ns.useplus.org/ldf/xmp/1.0/'; static const pmtm = 'http://www.hdrsoft.com/photomatix_settings01'; static const rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; + static const stCamera = 'http://ns.adobe.com/photoshop/1.0/camera-profile'; static const stEvt = 'http://ns.adobe.com/xap/1.0/sType/ResourceEvent#'; static const stRef = 'http://ns.adobe.com/xap/1.0/sType/ResourceRef#'; static const tiff = 'http://ns.adobe.com/tiff/1.0/'; @@ -96,7 +99,8 @@ class Namespaces { gDepth: 'Google Depth', gFocus: 'Google Focus', gImage: 'Google Image', - gimp: 'GIMP', + gimp210: 'GIMP 2.10', + gimpXmp: 'GIMP', gPano: 'Google Panorama', gSpherical: 'Google Spherical', illustrator: 'Illustrator', @@ -122,6 +126,7 @@ class Namespaces { xmpBJ: 'Basic Job Ticket', xmpDM: 'Dynamic Media', xmpMM: 'Media Management', + xmpNote: 'Note', xmpRights: 'Rights Management', xmpTPg: 'Paged-Text', }; diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index 752aecce5..d317996d9 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -128,7 +128,7 @@ class LicenseRow extends StatelessWidget { @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; - final bodyTextStyle = textTheme.bodyText2!; + final bodyTextStyle = textTheme.bodyMedium!; final subColor = bodyTextStyle.color!.withOpacity(.6); return Padding( diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 86510c002..d551d1823 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -105,11 +105,11 @@ class AvesApp extends StatefulWidget { class _AvesAppState extends State with WidgetsBindingObserver { final ValueNotifier appModeNotifier = ValueNotifier(AppMode.main); late final Future _appSetup; - late final Size _screenSize; late final Future _dynamicColorPaletteLoader; final CollectionSource _mediaStoreSource = MediaStoreSource(); final Debouncer _mediaStoreChangeDebouncer = Debouncer(delay: Durations.mediaContentChangeDebounceDelay); final Set _changedUris = {}; + Size? _screenSize; // observers are not registered when using the same list object with different items // the list itself needs to be reassigned @@ -119,15 +119,13 @@ class _AvesAppState extends State with WidgetsBindingObserver { final EventChannel _analysisCompletionChannel = const OptionalEventChannel('deckers.thibault/aves/analysis_events'); final EventChannel _errorChannel = const OptionalEventChannel('deckers.thibault/aves/error'); - Widget getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage(); - @override void initState() { super.initState(); EquatableConfig.stringify = true; _appSetup = _setup(); // remember screen size to use it later, when `context` and `window` are no longer reliable - _screenSize = window.physicalSize / window.devicePixelRatio; + _screenSize = _getScreenSize(); _dynamicColorPaletteLoader = DynamicColorPlugin.getCorePalette(); _mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChange(event as String?)); _newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?)); @@ -159,7 +157,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { AvesApp.showSystemUI(); } final home = initialized - ? getFirstPage() + ? _getFirstPage() : Scaffold( body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(), ); @@ -291,23 +289,31 @@ class _AvesAppState extends State with WidgetsBindingObserver { } } + Widget _getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage(); + + Size? _getScreenSize() { + final physicalSize = window.physicalSize; + final ratio = window.devicePixelRatio; + return physicalSize > Size.zero && ratio > 0 ? physicalSize / ratio : null; + } + // save IDs of entries visible at the top of the collection page with current layout settings void _saveTopEntries() { if (!settings.initialized) return; - final stopwatch = Stopwatch()..start(); + final screenSize = _screenSize ?? _getScreenSize(); + if (screenSize == null) return; var tileExtent = settings.getTileExtent(CollectionPage.routeName); if (tileExtent == 0) { - tileExtent = _screenSize.shortestSide / CollectionGrid.columnCountDefault; + tileExtent = screenSize.shortestSide / CollectionGrid.columnCountDefault; } - final rows = (_screenSize.height / tileExtent).ceil(); - final columns = (_screenSize.width / tileExtent).ceil(); + final rows = (screenSize.height / tileExtent).ceil(); + final columns = (screenSize.width / tileExtent).ceil(); final count = rows * columns; final collection = CollectionLens(source: _mediaStoreSource, listenToSource: false); settings.topEntryIds = collection.sortedEntries.take(count).map((entry) => entry.id).toList(); collection.dispose(); - debugPrint('Saved $count top entries in ${stopwatch.elapsed.inMilliseconds}ms'); } // setup before the first page is displayed. keep it short @@ -374,10 +380,10 @@ class _AvesAppState extends State with WidgetsBindingObserver { 'locales': WidgetsBinding.instance.window.locales.join(', '), 'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})', }); - _navigatorObservers = [ - AvesApp.pageRouteObserver, - ReportingRouteTracker(), - ]; + setState(() => _navigatorObservers = [ + AvesApp.pageRouteObserver, + ReportingRouteTracker(), + ]); } void _onNewIntent(Map? intentData) { @@ -389,7 +395,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { reportService.log('New intent'); AvesApp.navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute( settings: const RouteSettings(name: HomePage.routeName), - builder: (_) => getFirstPage(intentData: intentData), + builder: (_) => _getFirstPage(intentData: intentData), )); } diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 4a2d6ccdf..9d8658651 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -29,6 +29,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_app_bar.dart'; import 'package:aves/widgets/common/search/route.dart'; import 'package:aves/widgets/dialogs/tile_view_dialog.dart'; +import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -83,6 +84,7 @@ class _CollectionAppBarState extends State with SingleTickerPr ]; static const _layoutOptions = [ + TileLayout.mosaic, TileLayout.grid, TileLayout.list, ]; @@ -167,10 +169,16 @@ class _CollectionAppBarState extends State with SingleTickerPr bottom: Column( children: [ if (showFilterBar) - FilterBar( - filters: visibleFilters, - removable: removableFilters, - onTap: removableFilters ? collection.removeFilter : null, + NotificationListener( + onNotification: (notification) { + collection.addFilter(notification.reversedFilter); + return true; + }, + child: FilterBar( + filters: visibleFilters, + removable: removableFilters, + onTap: removableFilters ? collection.removeFilter : null, + ), ), if (queryEnabled) EntryQueryBar( @@ -310,7 +318,7 @@ class _CollectionAppBarState extends State with SingleTickerPr title: context.l10n.collectionActionEdit, items: [ _buildRotateAndFlipMenuItems(context, canApply: canApply), - ...EntrySetActions.edit.where(isVisible).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)), + ...EntrySetActions.edit.where((v) => isVisible(v) && !selectionQuickActions.contains(v)).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)), ], ), ), @@ -537,9 +545,9 @@ class _CollectionAppBarState extends State with SingleTickerPr builder: (context) { return TileViewDialog( initialValue: initialValue, - sortOptions: Map.fromEntries(_sortOptions.map((v) => MapEntry(v, v.getName(context)))), - groupOptions: Map.fromEntries(_groupOptions.map((v) => MapEntry(v, v.getName(context)))), - layoutOptions: Map.fromEntries(_layoutOptions.map((v) => MapEntry(v, v.getName(context)))), + sortOptions: _sortOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(), + groupOptions: _groupOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(), + layoutOptions: _layoutOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(), sortOrder: (factor, reverse) => factor.getOrderName(context, reverse), canGroup: (s, g, l) => s == EntrySortFactor.date, ); diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 39b161ac0..c75778274 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -7,6 +7,7 @@ import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.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/source/enums/enums.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/ref/mime_types.dart'; @@ -25,7 +26,9 @@ import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/grid/draggable_thumb_label.dart'; import 'package:aves/widgets/common/grid/item_tracker.dart'; import 'package:aves/widgets/common/grid/scaling.dart'; -import 'package:aves/widgets/common/grid/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/fixed/scale_grid.dart'; +import 'package:aves/widgets/common/grid/sections/list_layout.dart'; +import 'package:aves/widgets/common/grid/sections/section_layout.dart'; import 'package:aves/widgets/common/grid/selector.dart'; import 'package:aves/widgets/common/grid/sliver.dart'; import 'package:aves/widgets/common/grid/theme.dart'; @@ -34,6 +37,7 @@ import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart'; import 'package:aves/widgets/common/thumbnail/decorated.dart'; +import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart'; import 'package:flutter/gestures.dart'; @@ -50,7 +54,8 @@ class CollectionGrid extends StatefulWidget { static const int columnCountDefault = 4; static const double extentMin = 46; static const double extentMax = 300; - static const double spacing = 2; + static const double fixedExtentLayoutSpacing = 2; + static const double mosaicLayoutSpacing = 4; const CollectionGrid({ super.key, @@ -64,6 +69,8 @@ class CollectionGrid extends StatefulWidget { class _CollectionGridState extends State { TileExtentController? _tileExtentController; + String get settingsRouteKey => widget.settingsRouteKey; + @override void dispose() { _tileExtentController?.dispose(); @@ -72,14 +79,17 @@ class _CollectionGridState extends State { @override Widget build(BuildContext context) { - _tileExtentController ??= TileExtentController( - settingsRouteKey: widget.settingsRouteKey, - columnCountDefault: CollectionGrid.columnCountDefault, - extentMin: CollectionGrid.extentMin, - extentMax: CollectionGrid.extentMax, - spacing: CollectionGrid.spacing, - horizontalPadding: 2, - ); + final spacing = context.select((s) => s.getTileLayout(settingsRouteKey) == TileLayout.mosaic ? CollectionGrid.mosaicLayoutSpacing : CollectionGrid.fixedExtentLayoutSpacing); + if (_tileExtentController?.spacing != spacing) { + _tileExtentController = TileExtentController( + settingsRouteKey: settingsRouteKey, + columnCountDefault: CollectionGrid.columnCountDefault, + extentMin: CollectionGrid.extentMin, + extentMax: CollectionGrid.extentMax, + spacing: spacing, + horizontalPadding: 2, + ); + } return TileExtentControllerProvider( controller: _tileExtentController!, child: _CollectionGridContent(), @@ -108,12 +118,13 @@ class _CollectionGridContent extends StatelessWidget { final columnCount = c.item2; final tileSpacing = c.item3; final horizontalPadding = c.item4; + final source = collection.source; return GridTheme( extent: thumbnailExtent, child: EntryListDetailsTheme( extent: thumbnailExtent, child: ValueListenableBuilder( - valueListenable: collection.source.stateNotifier, + valueListenable: source.stateNotifier, builder: (context, sourceState, child) { late final Duration tileAnimationDelay; if (sourceState == SourceState.ready) { @@ -123,30 +134,37 @@ class _CollectionGridContent extends StatelessWidget { } else { tileAnimationDelay = Duration.zero; } - return SectionedEntryListLayoutProvider( - collection: collection, - selectable: selectable, - scrollableWidth: scrollableWidth, - tileLayout: tileLayout, - columnCount: columnCount, - spacing: tileSpacing, - horizontalPadding: horizontalPadding, - tileExtent: thumbnailExtent, - tileBuilder: (entry) => AnimatedBuilder( - animation: favourites, - builder: (context, child) { - return InteractiveTile( - key: ValueKey(entry.id), - collection: collection, - entry: entry, - thumbnailExtent: thumbnailExtent, - tileLayout: tileLayout, - isScrollingNotifier: _isScrollingNotifier, + + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) => SectionedEntryListLayoutProvider( + collection: collection, + selectable: selectable, + scrollableWidth: scrollableWidth, + tileLayout: tileLayout, + columnCount: columnCount, + spacing: tileSpacing, + horizontalPadding: horizontalPadding, + tileExtent: thumbnailExtent, + tileBuilder: (entry, tileSize) { + final extent = tileSize.shortestSide; + return AnimatedBuilder( + animation: favourites, + builder: (context, child) { + return InteractiveTile( + key: ValueKey(entry.id), + collection: collection, + entry: entry, + thumbnailExtent: extent, + tileLayout: tileLayout, + isScrollingNotifier: _isScrollingNotifier, + ); + }, ); }, + tileAnimationDelay: tileAnimationDelay, + child: child!, ), - tileAnimationDelay: tileAnimationDelay, - child: child!, ); }, child: child, @@ -260,12 +278,13 @@ class _CollectionScaler extends StatelessWidget { final metrics = context.select>((v) => Tuple2(v.spacing, v.horizontalPadding)); final tileSpacing = metrics.item1; final horizontalPadding = metrics.item2; + final brightness = Theme.of(context).brightness; return GridScaleGestureDetector( scrollableKey: scrollableKey, tileLayout: tileLayout, heightForWidth: (width) => width, gridBuilder: (center, tileSize, child) => CustomPaint( - painter: GridPainter( + painter: FixedExtentGridPainter( tileLayout: tileLayout, tileCenter: center, tileSize: tileSize, @@ -278,7 +297,7 @@ class _CollectionScaler extends StatelessWidget { ), child: child, ), - scaledBuilder: (entry, tileSize) => EntryListDetailsTheme( + scaledItemBuilder: (entry, tileSize) => EntryListDetailsTheme( extent: tileSize.height, child: Tile( entry: entry, @@ -286,6 +305,15 @@ class _CollectionScaler extends StatelessWidget { tileLayout: tileLayout, ), ), + mosaicItemBuilder: (index, targetExtent) => DecoratedBox( + decoration: BoxDecoration( + color: ThumbnailImage.computeLoadingBackgroundColor(index * 10, brightness).withOpacity(.9), + border: Border.all( + color: DecoratedThumbnail.borderColor, + width: DecoratedThumbnail.borderWidth, + ), + ), + ), child: child, ); } diff --git a/lib/widgets/collection/draggable_thumb_label.dart b/lib/widgets/collection/draggable_thumb_label.dart index 145a00607..d04670818 100644 --- a/lib/widgets/collection/draggable_thumb_label.dart +++ b/lib/widgets/collection/draggable_thumb_label.dart @@ -6,7 +6,7 @@ import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/draggable_thumb_label.dart'; -import 'package:aves/widgets/common/grid/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/list_layout.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index c21691f17..420faab2d 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -370,6 +370,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware Set obsoleteTags = todoItems.expand((entry) => entry.tags).toSet(); Set obsoleteCountryCodes = todoItems.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.countryCode).whereNotNull().toSet(); + final Set dataTypes = {}; final source = context.read(); source.pauseMonitoring(); var cancelled = false; @@ -379,8 +380,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware if (cancelled) { return ImageOpEvent(success: true, skipped: true, uri: entry.uri); } else { - final dataTypes = await op(entry); - return ImageOpEvent(success: dataTypes.isNotEmpty, skipped: false, uri: entry.uri); + final opDataTypes = await op(entry); + dataTypes.addAll(opDataTypes); + return ImageOpEvent(success: opDataTypes.isNotEmpty, skipped: false, uri: entry.uri); } }).asBroadcastStream(), itemCount: todoCount, @@ -402,6 +404,10 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware } })); + if (dataTypes.contains(EntryDataType.aspectRatio)) { + source.onAspectRatioChanged(); + } + if (showResult) { final l10n = context.l10n; final successCount = successOps.length; diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart index 5170fff8e..dfbb3b873 100644 --- a/lib/widgets/collection/filter_bar.dart +++ b/lib/widgets/collection/filter_bar.dart @@ -64,7 +64,7 @@ class _FilterBarState extends State { ), ); } - : (context, animation) => _buildChip(filter), + : (context, animation) => const SizedBox(), duration: animate ? Durations.filterBarRemovalAnimation : Duration.zero, ); }); diff --git a/lib/widgets/collection/grid/list_details_theme.dart b/lib/widgets/collection/grid/list_details_theme.dart index e6bd16e8c..adbdcb2a1 100644 --- a/lib/widgets/collection/grid/list_details_theme.dart +++ b/lib/widgets/collection/grid/list_details_theme.dart @@ -29,8 +29,8 @@ class EntryListDetailsTheme extends StatelessWidget { final textScaleFactor = mq.textScaleFactor; final textTheme = Theme.of(context).textTheme; - final titleStyle = textTheme.bodyText2!; - final captionStyle = textTheme.caption!; + final titleStyle = textTheme.bodyMedium!; + final captionStyle = textTheme.bodySmall!; final titleLineHeight = (RenderParagraph( TextSpan(text: 'Fake Title', style: titleStyle), diff --git a/lib/widgets/collection/grid/section_layout.dart b/lib/widgets/collection/grid/section_layout.dart index 6ca41975c..276861e32 100644 --- a/lib/widgets/collection/grid/section_layout.dart +++ b/lib/widgets/collection/grid/section_layout.dart @@ -2,14 +2,14 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/widgets/collection/grid/headers/any.dart'; -import 'package:aves/widgets/common/grid/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/provider.dart'; import 'package:flutter/material.dart'; class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider { final CollectionLens collection; final bool selectable; - const SectionedEntryListLayoutProvider({ + SectionedEntryListLayoutProvider({ super.key, required this.collection, required this.selectable, @@ -25,6 +25,7 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider item.displayAspectRatio, ); @override diff --git a/lib/widgets/collection/grid/tile.dart b/lib/widgets/collection/grid/tile.dart index 3b4ed5225..c37693b3b 100644 --- a/lib/widgets/collection/grid/tile.dart +++ b/lib/widgets/collection/grid/tile.dart @@ -122,6 +122,7 @@ class Tile extends StatelessWidget { @override Widget build(BuildContext context) { switch (tileLayout) { + case TileLayout.mosaic: case TileLayout.grid: return _buildThumbnail(); case TileLayout.list: @@ -145,6 +146,7 @@ class Tile extends StatelessWidget { Widget _buildThumbnail() => DecoratedThumbnail( entry: entry, tileExtent: thumbnailExtent, + isMosaic: tileLayout == TileLayout.mosaic, // when the user is scrolling faster than we can retrieve the thumbnails, // the retrieval task queue can pile up for thumbnails that got disposed // in this case we pause the image retrieval task to get it out of the queue diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index a0694c98b..eff79b795 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -8,6 +8,7 @@ import 'package:aves/services/accessibility_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/action_mixins/overlay_snack_bar.dart'; +import 'package:aves/widgets/common/basic/animated_text.dart'; import 'package:aves/widgets/common/basic/circle.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; @@ -343,7 +344,8 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro Widget build(BuildContext context) { final text = Text(widget.message); final theme = Theme.of(context); - final contentTextStyle = theme.snackBarTheme.contentTextStyle ?? ThemeData(brightness: theme.brightness).textTheme.subtitle1; + final contentTextStyle = theme.snackBarTheme.contentTextStyle ?? ThemeData(brightness: theme.brightness).textTheme.titleMedium!; + final timerChangeShadowColor = theme.colorScheme.primary; return _remainingDurationMillis == null ? text : Row( @@ -362,9 +364,25 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro // progress color is provided by the caller, // because we cannot use the app context theme here foreground: widget.progressColor, - center: Text( + center: AnimatedText( '${(remainingDurationMillis / 1000).ceil()}', - style: contentTextStyle, + style: contentTextStyle.copyWith( + shadows: [ + Shadow( + color: timerChangeShadowColor.withOpacity(0), + blurRadius: 0, + ) + ], + ), + changedStyle: contentTextStyle.copyWith( + shadows: [ + Shadow( + color: timerChangeShadowColor, + blurRadius: 5, + ) + ], + ), + duration: context.read().formTextStyleTransition, ), ); }, diff --git a/lib/widgets/common/action_mixins/overlay_snack_bar.dart b/lib/widgets/common/action_mixins/overlay_snack_bar.dart index 3ba3d78d8..778654cca 100644 --- a/lib/widgets/common/action_mixins/overlay_snack_bar.dart +++ b/lib/widgets/common/action_mixins/overlay_snack_bar.dart @@ -48,7 +48,7 @@ class OverlaySnackBar extends StatelessWidget { ), ); - final contentTextStyle = snackBarTheme.contentTextStyle ?? ThemeData(brightness: brightness).textTheme.subtitle1; + final contentTextStyle = snackBarTheme.contentTextStyle ?? ThemeData(brightness: brightness).textTheme.titleMedium; final horizontalPadding = FeedbackMixin.snackBarHorizontalPadding(snackBarTheme); final padding = EdgeInsetsDirectional.only(start: horizontalPadding, end: action != null ? 0 : horizontalPadding); diff --git a/lib/widgets/common/app_bar/app_bar_subtitle.dart b/lib/widgets/common/app_bar/app_bar_subtitle.dart index 2e09fe94c..a021b149c 100644 --- a/lib/widgets/common/app_bar/app_bar_subtitle.dart +++ b/lib/widgets/common/app_bar/app_bar_subtitle.dart @@ -66,7 +66,7 @@ class SourceStateSubtitle extends StatelessWidget { final theme = Theme.of(context); return DefaultTextStyle.merge( - style: theme.textTheme.caption!.copyWith(fontFeatures: const [FontFeature.disable('smcp')]), + style: theme.textTheme.bodySmall!.copyWith(fontFeatures: const [FontFeature.disable('smcp')]), child: ValueListenableBuilder( valueListenable: source.progressNotifier, builder: (context, progress, snapshot) { diff --git a/lib/widgets/common/app_bar/sliver_app_bar_title.dart b/lib/widgets/common/app_bar/sliver_app_bar_title.dart index 3c04c231f..2db5f9bb6 100644 --- a/lib/widgets/common/app_bar/sliver_app_bar_title.dart +++ b/lib/widgets/common/app_bar/sliver_app_bar_title.dart @@ -14,7 +14,7 @@ class SliverAppBarTitleWrapper extends StatelessWidget { @override Widget build(BuildContext context) { final toolbarOpacity = context.dependOnInheritedWidgetOfExactType()!.toolbarOpacity; - final baseColor = (DefaultTextStyle.of(context).style.color ?? Theme.of(context).textTheme.headline6!.color!); + final baseColor = (DefaultTextStyle.of(context).style.color ?? Theme.of(context).textTheme.titleLarge!.color!); return DefaultTextStyle.merge( style: TextStyle(color: baseColor.withOpacity(toolbarOpacity)), child: child, diff --git a/lib/widgets/common/basic/animated_text.dart b/lib/widgets/common/basic/animated_text.dart new file mode 100644 index 000000000..80c7c04a9 --- /dev/null +++ b/lib/widgets/common/basic/animated_text.dart @@ -0,0 +1,86 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +class AnimatedText extends StatefulWidget { + final String data; + final TextStyle style, changedStyle; + final Curve curve; + final Duration duration; + + const AnimatedText( + this.data, { + super.key, + required this.style, + required this.changedStyle, + this.curve = Curves.linear, + required this.duration, + }); + + @override + State createState() => _AnimatedTextState(); +} + +class _AnimatedTextState extends State with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _style; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: widget.duration, + vsync: this, + ) + ..value = 1 + ..addListener(() => setState(() {})); + _style = _ShadowedTextStyleTween(begin: widget.changedStyle, end: widget.style).animate(CurvedAnimation( + parent: _controller, + curve: widget.curve, + )); + } + + @override + void didUpdateWidget(AnimatedText oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.data != widget.data) { + _controller + ..value = 0 + ..forward(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Text( + widget.data, + style: _style.value, + ); + } +} + +class _ShadowedTextStyleTween extends Tween { + _ShadowedTextStyleTween({super.begin, super.end}); + + @override + TextStyle lerp(double t) { + final textStyle = TextStyle.lerp(begin, end, t)!; + final beginShadows = begin!.shadows; + final endShadows = end!.shadows; + if (beginShadows != null && endShadows != null && beginShadows.length == endShadows.length) { + return textStyle.copyWith( + shadows: beginShadows.mapIndexed((i, a) { + final b = endShadows[i]; + return Shadow.lerp(a, b, t)!; + }).toList(), + ); + } else { + return textStyle; + } + } +} diff --git a/lib/widgets/common/basic/circle.dart b/lib/widgets/common/basic/circle.dart index d829c6993..0b3fdba15 100644 --- a/lib/widgets/common/basic/circle.dart +++ b/lib/widgets/common/basic/circle.dart @@ -29,13 +29,13 @@ class _CircularIndicatorState extends State { child: Stack( alignment: Alignment.center, children: [ - Circle( + _Circle( radius: widget.radius, lineWidth: widget.lineWidth, percent: 1.0, color: widget.background, ), - Circle( + _Circle( radius: widget.radius, lineWidth: widget.lineWidth, percent: widget.percent, @@ -48,12 +48,11 @@ class _CircularIndicatorState extends State { } } -class Circle extends StatelessWidget { +class _Circle extends StatelessWidget { final double radius, lineWidth, percent; final Color color; - const Circle({ - super.key, + const _Circle({ required this.radius, required this.lineWidth, required this.percent, diff --git a/lib/widgets/common/basic/menu.dart b/lib/widgets/common/basic/menu.dart index ff589e4e6..d9b95d2a3 100644 --- a/lib/widgets/common/basic/menu.dart +++ b/lib/widgets/common/basic/menu.dart @@ -82,7 +82,7 @@ class _PopupMenuItemExpansionPanelState extends State { @override void didUpdateWidget(covariant MultiCrossFader oldWidget) { super.didUpdateWidget(oldWidget); - if (_first == oldWidget.child) { + if (oldWidget.child == _first) { _second = widget.child; _fadeState = CrossFadeState.showSecond; } else { diff --git a/lib/widgets/common/basic/query_bar.dart b/lib/widgets/common/basic/query_bar.dart index d525edeb8..9cde4c3cb 100644 --- a/lib/widgets/common/basic/query_bar.dart +++ b/lib/widgets/common/basic/query_bar.dart @@ -52,7 +52,7 @@ class _QueryBarState extends State { ); return DefaultTextStyle( - style: Theme.of(context).textTheme.bodyText2!, + style: Theme.of(context).textTheme.bodyMedium!, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/widgets/common/basic/slider_list_tile.dart b/lib/widgets/common/basic/slider_list_tile.dart index 8997b687a..337e24e63 100644 --- a/lib/widgets/common/basic/slider_list_tile.dart +++ b/lib/widgets/common/basic/slider_list_tile.dart @@ -29,7 +29,7 @@ class SliderListTile extends StatelessWidget { ), ), child: DefaultTextStyle( - style: Theme.of(context).textTheme.subtitle1!, + style: Theme.of(context).textTheme.titleMedium!, child: Padding( padding: const EdgeInsets.only(top: 16, bottom: 8), child: Column( diff --git a/lib/widgets/common/basic/text_dropdown_button.dart b/lib/widgets/common/basic/text_dropdown_button.dart index 4f60ef164..8efc50028 100644 --- a/lib/widgets/common/basic/text_dropdown_button.dart +++ b/lib/widgets/common/basic/text_dropdown_button.dart @@ -5,6 +5,7 @@ class TextDropdownButton extends DropdownButton { super.key, required List values, required String Function(T value) valueText, + IconData Function(T value)? valueIcon, super.value, super.hint, super.disabledHint, @@ -32,21 +33,51 @@ class TextDropdownButton extends DropdownButton { items: values .map((v) => DropdownMenuItem( value: v, - child: Text(valueText(v)), + child: _buildItem(valueText(v), valueIcon?.call(v), selected: false), )) .toList(), selectedItemBuilder: (context) => values .map((v) => DropdownMenuItem( value: v, - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Text( - valueText(v), - softWrap: false, - overflow: TextOverflow.fade, - ), - ), + child: _buildItem(valueText(v), valueIcon?.call(v), selected: true), )) .toList(), ); + + static Widget _buildItem(String text, IconData? icon, {required bool selected}) { + final softWrap = selected ? false : null; + final overflow = selected ? TextOverflow.fade : null; + + Widget child = icon != null + ? Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: const EdgeInsetsDirectional.only(end: 8, bottom: 2), + child: Icon(icon), + ), + ), + TextSpan(text: text), + ], + ), + softWrap: softWrap, + overflow: overflow, + ) + : Text( + text, + softWrap: softWrap, + overflow: overflow, + ); + + if (selected) { + child = Align( + alignment: AlignmentDirectional.centerStart, + child: child, + ); + } + + return child; + } } diff --git a/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart b/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart index d9b6f0df3..9149e0b63 100644 --- a/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart +++ b/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart @@ -4,7 +4,7 @@ import 'dart:math' as math; import 'package:flutter/gestures.dart'; // adapted from Flutter `ScaleGestureRecognizer` in `/gestures/scale.dart` -// ignore_for_file: curly_braces_in_flow_control_structures, deprecated_member_use, unnecessary_null_comparison +// ignore_for_file: avoid_types_on_closure_parameters, unnecessary_null_comparison /// The possible states of a [ScaleGestureRecognizer]. enum _ScaleState { @@ -25,6 +25,17 @@ enum _ScaleState { started, } +class _PointerPanZoomData { + _PointerPanZoomData({required this.focalPoint, required this.scale, required this.rotation}); + + Offset focalPoint; + double scale; + double rotation; + + @override + String toString() => '_PointerPanZoomData(focalPoint: $focalPoint, scale: $scale, angle: $rotation)'; +} + //////////////////////////////////////////////////////////////////////////////// bool _isFlingGesture(Velocity velocity) { @@ -71,20 +82,15 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { /// /// {@macro flutter.gestures.GestureRecognizer.supportedDevices} EagerScaleGestureRecognizer({ - Object? debugOwner, + super.debugOwner, @Deprecated( 'Migrate to supportedDevices. ' 'This feature was deprecated after v2.3.0-1.0.pre.', ) - PointerDeviceKind? kind, - Set? supportedDevices, + super.kind, + super.supportedDevices, this.dragStartBehavior = DragStartBehavior.down, - }) : assert(dragStartBehavior != null), - super( - debugOwner: debugOwner, - kind: kind, - supportedDevices: supportedDevices, - ); + }) : assert(dragStartBehavior != null); /// Determines what point is used as the starting point in all calculations /// involving this gesture. @@ -142,35 +148,71 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { late Offset _localFocalPoint; _LineBetweenPointers? _initialLine; _LineBetweenPointers? _currentLine; - late Map _pointerLocations; - late List _pointerQueue; // A queue to sort pointers in order of entrance + final Map _pointerLocations = {}; + final List _pointerQueue = []; // A queue to sort pointers in order of entrance final Map _velocityTrackers = {}; late Offset _delta; + final Map _pointerPanZooms = {}; + double _initialPanZoomScaleFactor = 1; + double _initialPanZoomRotationFactor = 0; - double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0; + double get _pointerScaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0; - double get _horizontalScaleFactor => _initialHorizontalSpan > 0.0 ? _currentHorizontalSpan / _initialHorizontalSpan : 1.0; + double get _pointerHorizontalScaleFactor => _initialHorizontalSpan > 0.0 ? _currentHorizontalSpan / _initialHorizontalSpan : 1.0; - double get _verticalScaleFactor => _initialVerticalSpan > 0.0 ? _currentVerticalSpan / _initialVerticalSpan : 1.0; + double get _pointerVerticalScaleFactor => _initialVerticalSpan > 0.0 ? _currentVerticalSpan / _initialVerticalSpan : 1.0; + + double get _scaleFactor { + double scale = _pointerScaleFactor; + for (final _PointerPanZoomData p in _pointerPanZooms.values) { + scale *= p.scale / _initialPanZoomScaleFactor; + } + return scale; + } + + double get _horizontalScaleFactor { + double scale = _pointerHorizontalScaleFactor; + for (final _PointerPanZoomData p in _pointerPanZooms.values) { + scale *= p.scale / _initialPanZoomScaleFactor; + } + return scale; + } + + double get _verticalScaleFactor { + double scale = _pointerVerticalScaleFactor; + for (final _PointerPanZoomData p in _pointerPanZooms.values) { + scale *= p.scale / _initialPanZoomScaleFactor; + } + return scale; + } + + int get _pointerCount { + return _pointerPanZooms.length + _pointerQueue.length; + } double _computeRotationFactor() { - if (_initialLine == null || _currentLine == null) { - return 0.0; + double factor = 0.0; + if (_initialLine != null && _currentLine != null) { + final double fx = _initialLine!.pointerStartLocation.dx; + final double fy = _initialLine!.pointerStartLocation.dy; + final double sx = _initialLine!.pointerEndLocation.dx; + final double sy = _initialLine!.pointerEndLocation.dy; + + final double nfx = _currentLine!.pointerStartLocation.dx; + final double nfy = _currentLine!.pointerStartLocation.dy; + final double nsx = _currentLine!.pointerEndLocation.dx; + final double nsy = _currentLine!.pointerEndLocation.dy; + + final double angle1 = math.atan2(fy - sy, fx - sx); + final double angle2 = math.atan2(nfy - nsy, nfx - nsx); + + factor = angle2 - angle1; } - final double fx = _initialLine!.pointerStartLocation.dx; - final double fy = _initialLine!.pointerStartLocation.dy; - final double sx = _initialLine!.pointerEndLocation.dx; - final double sy = _initialLine!.pointerEndLocation.dy; - - final double nfx = _currentLine!.pointerStartLocation.dx; - final double nfy = _currentLine!.pointerStartLocation.dy; - final double nsx = _currentLine!.pointerEndLocation.dx; - final double nsy = _currentLine!.pointerEndLocation.dy; - - final double angle1 = math.atan2(fy - sy, fx - sx); - final double angle2 = math.atan2(nfy - nsy, nfx - nsx); - - return angle2 - angle1; + for (final _PointerPanZoomData p in _pointerPanZooms.values) { + factor += p.rotation; + } + factor -= _initialPanZoomRotationFactor; + return factor; } @override @@ -185,8 +227,21 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { _currentHorizontalSpan = 0.0; _initialVerticalSpan = 0.0; _currentVerticalSpan = 0.0; - _pointerLocations = {}; - _pointerQueue = []; + } + } + + @override + bool isPointerPanZoomAllowed(PointerPanZoomStartEvent event) => true; + + @override + void addAllowedPointerPanZoom(PointerPanZoomStartEvent event) { + super.addAllowedPointerPanZoom(event); + startTrackingPointer(event.pointer, event.transform); + _velocityTrackers[event.pointer] = VelocityTracker.withKind(event.kind); + if (_state == _ScaleState.ready) { + _state = _ScaleState.possible; + _initialPanZoomScaleFactor = 1.0; + _initialPanZoomRotationFactor = 0.0; } } @@ -197,7 +252,9 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { bool shouldStartIfAccepted = false; if (event is PointerMoveEvent) { final VelocityTracker tracker = _velocityTrackers[event.pointer]!; - if (!event.synthesized) tracker.addPosition(event.timeStamp, event.position); + if (!event.synthesized) { + tracker.addPosition(event.timeStamp, event.position); + } _pointerLocations[event.pointer] = event.position; shouldStartIfAccepted = true; _lastTransform = event.transform; @@ -212,24 +269,46 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { _pointerQueue.remove(event.pointer); didChangeConfiguration = true; _lastTransform = event.transform; + } else if (event is PointerPanZoomStartEvent) { + assert(_pointerPanZooms[event.pointer] == null); + _pointerPanZooms[event.pointer] = _PointerPanZoomData(focalPoint: event.position, scale: 1, rotation: 0); + didChangeConfiguration = true; + shouldStartIfAccepted = true; + } else if (event is PointerPanZoomUpdateEvent) { + assert(_pointerPanZooms[event.pointer] != null); + if (!event.synthesized) { + _velocityTrackers[event.pointer]!.addPosition(event.timeStamp, event.pan); + } + _pointerPanZooms[event.pointer] = _PointerPanZoomData(focalPoint: event.position + event.pan, scale: event.scale, rotation: event.rotation); + _lastTransform = event.transform; + shouldStartIfAccepted = true; + } else if (event is PointerPanZoomEndEvent) { + assert(_pointerPanZooms[event.pointer] != null); + _pointerPanZooms.remove(event.pointer); + didChangeConfiguration = true; } _updateLines(); _update(); - if (!didChangeConfiguration || _reconfigure(event.pointer)) _advanceStateMachine(shouldStartIfAccepted, event.kind); + if (!didChangeConfiguration || _reconfigure(event.pointer)) { + _advanceStateMachine(shouldStartIfAccepted, event.kind); + } stopTrackingIfPointerNoLongerDown(event); } void _update() { - final int count = _pointerLocations.keys.length; - final Offset? previousFocalPoint = _currentFocalPoint; // Compute the focal point Offset focalPoint = Offset.zero; - for (final int pointer in _pointerLocations.keys) focalPoint += _pointerLocations[pointer]!; - _currentFocalPoint = count > 0 ? focalPoint / count.toDouble() : Offset.zero; + for (final int pointer in _pointerLocations.keys) { + focalPoint += _pointerLocations[pointer]!; + } + for (final _PointerPanZoomData p in _pointerPanZooms.values) { + focalPoint += p.focalPoint; + } + _currentFocalPoint = _pointerCount > 0 ? focalPoint / _pointerCount.toDouble() : Offset.zero; if (previousFocalPoint == null) { _localFocalPoint = PointerEvent.transformPosition( @@ -246,6 +325,16 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { _delta = _localFocalPoint - localPreviousFocalPoint; } + final int count = _pointerLocations.keys.length; + + Offset pointerFocalPoint = Offset.zero; + for (final int pointer in _pointerLocations.keys) { + pointerFocalPoint += _pointerLocations[pointer]!; + } + if (count > 0) { + pointerFocalPoint = pointerFocalPoint / count.toDouble(); + } + // Span is the average deviation from focal point. Horizontal and vertical // spans are the average deviations from the focal point's horizontal and // vertical coordinates, respectively. @@ -253,9 +342,9 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { double totalHorizontalDeviation = 0.0; double totalVerticalDeviation = 0.0; for (final int pointer in _pointerLocations.keys) { - totalDeviation += (_currentFocalPoint! - _pointerLocations[pointer]!).distance; - totalHorizontalDeviation += (_currentFocalPoint!.dx - _pointerLocations[pointer]!.dx).abs(); - totalVerticalDeviation += (_currentFocalPoint!.dy - _pointerLocations[pointer]!.dy).abs(); + totalDeviation += (pointerFocalPoint - _pointerLocations[pointer]!).distance; + totalHorizontalDeviation += (pointerFocalPoint.dx - _pointerLocations[pointer]!.dx).abs(); + totalVerticalDeviation += (pointerFocalPoint.dy - _pointerLocations[pointer]!.dy).abs(); } _currentSpan = count > 0 ? totalDeviation / count : 0.0; _currentHorizontalSpan = count > 0 ? totalHorizontalDeviation / count : 0.0; @@ -297,6 +386,13 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { _initialLine = _currentLine; _initialHorizontalSpan = _currentHorizontalSpan; _initialVerticalSpan = _currentVerticalSpan; + if (_pointerPanZooms.isEmpty) { + _initialPanZoomScaleFactor = 1.0; + _initialPanZoomRotationFactor = 0.0; + } else { + _initialPanZoomScaleFactor = _scaleFactor / _pointerScaleFactor; + _initialPanZoomRotationFactor = _pointerPanZooms.values.map((_PointerPanZoomData x) => x.rotation).reduce((double a, double b) => a + b); + } if (_state == _ScaleState.started) { if (onEnd != null) { final VelocityTracker tracker = _velocityTrackers[pointer]!; @@ -304,10 +400,12 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { Velocity velocity = tracker.getVelocity(); if (_isFlingGesture(velocity)) { final Offset pixelsPerSecond = velocity.pixelsPerSecond; - if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity); - invokeCallback('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerQueue.length))); + if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) { + velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity); + } + invokeCallback('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerCount))); } else { - invokeCallback('onEnd', () => onEnd!(ScaleEndDetails(pointerCount: _pointerQueue.length))); + invokeCallback('onEnd', () => onEnd!(ScaleEndDetails(pointerCount: _pointerCount))); } } _state = _ScaleState.accepted; @@ -317,7 +415,9 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { } void _advanceStateMachine(bool shouldStartIfAccepted, PointerDeviceKind pointerDeviceKind) { - if (_state == _ScaleState.ready) _state = _ScaleState.possible; + if (_state == _ScaleState.ready) { + _state = _ScaleState.possible; + } // TLAD insert start if (_pointerQueue.length == 2) { @@ -328,7 +428,9 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { if (_state == _ScaleState.possible) { final double spanDelta = (_currentSpan - _initialSpan).abs(); final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance; - if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind, gestureSettings)) resolve(GestureDisposition.accepted); + if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind, gestureSettings) || math.max(_scaleFactor / _pointerScaleFactor, _pointerScaleFactor / _scaleFactor) > 1.05) { + resolve(GestureDisposition.accepted); + } } else if (_state.index >= _ScaleState.accepted.index) { resolve(GestureDisposition.accepted); } @@ -338,7 +440,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { _dispatchOnStartCallbackIfNeeded(); } - if (_state == _ScaleState.started && onUpdate != null) + if (_state == _ScaleState.started && onUpdate != null) { invokeCallback('onUpdate', () { onUpdate!(ScaleUpdateDetails( scale: _scaleFactor, @@ -347,22 +449,24 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { focalPoint: _currentFocalPoint!, localFocalPoint: _localFocalPoint, rotation: _computeRotationFactor(), - pointerCount: _pointerQueue.length, + pointerCount: _pointerCount, focalPointDelta: _delta, )); }); + } } void _dispatchOnStartCallbackIfNeeded() { assert(_state == _ScaleState.started); - if (onStart != null) + if (onStart != null) { invokeCallback('onStart', () { onStart!(ScaleStartDetails( focalPoint: _currentFocalPoint!, localFocalPoint: _localFocalPoint, - pointerCount: _pointerQueue.length, + pointerCount: _pointerCount, )); }); + } } @override @@ -376,12 +480,22 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer { _initialLine = _currentLine; _initialHorizontalSpan = _currentHorizontalSpan; _initialVerticalSpan = _currentVerticalSpan; + if (_pointerPanZooms.isEmpty) { + _initialPanZoomScaleFactor = 1.0; + _initialPanZoomRotationFactor = 0.0; + } else { + _initialPanZoomScaleFactor = _scaleFactor / _pointerScaleFactor; + _initialPanZoomRotationFactor = _pointerPanZooms.values.map((_PointerPanZoomData x) => x.rotation).reduce((double a, double b) => a + b); + } } } } @override void rejectGesture(int pointer) { + _pointerPanZooms.remove(pointer); + _pointerLocations.remove(pointer); + _pointerQueue.remove(pointer); stopTrackingPointer(pointer); } diff --git a/lib/widgets/common/expandable_filter_row.dart b/lib/widgets/common/expandable_filter_row.dart index c8e32ab18..96b3f7fdc 100644 --- a/lib/widgets/common/expandable_filter_row.dart +++ b/lib/widgets/common/expandable_filter_row.dart @@ -30,7 +30,7 @@ class ExpandableFilterRow extends StatelessWidget { @override Widget build(BuildContext context) { - if (filters.isEmpty) return const SizedBox.shrink(); + if (filters.isEmpty) return const SizedBox(); final hasTitle = title != null && title!.isNotEmpty; diff --git a/lib/widgets/common/fx/transition_image.dart b/lib/widgets/common/fx/transition_image.dart index cad92fddb..4ee1b8390 100644 --- a/lib/widgets/common/fx/transition_image.dart +++ b/lib/widgets/common/fx/transition_image.dart @@ -3,15 +3,17 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -// adapted from Flutter `RawImage`, `paintImage()` from `DecorationImagePainter`, etc. +// adapted from Flutter `_ImageState` in `/widgets/image.dart` +// and `DecorationImagePainter` in `/painting/decoration_image.dart` // to transition between 2 different fits during hero animation: // - BoxFit.cover at t=0 // - BoxFit.contain at t=1 class TransitionImage extends StatefulWidget { final ImageProvider image; - final double? width, height; final ValueListenable animation; + final BoxFit thumbnailFit, viewerFit; + final double? width, height; final bool gaplessPlayback = false; final Color? background; @@ -19,6 +21,8 @@ class TransitionImage extends StatefulWidget { super.key, required this.image, required this.animation, + required this.thumbnailFit, + required this.viewerFit, this.width, this.height, this.background, @@ -34,11 +38,6 @@ class _TransitionImageState extends State { bool _isListeningToStream = false; int? _frameNumber; - @override - void initState() { - super.initState(); - } - @override void dispose() { assert(_imageStream != null); @@ -63,8 +62,9 @@ class _TransitionImageState extends State { void didUpdateWidget(covariant TransitionImage oldWidget) { super.didUpdateWidget(oldWidget); if (_isListeningToStream) { - _imageStream!.removeListener(_getListener()); - _imageStream!.addListener(_getListener()); + final ImageStreamListener oldListener = _getListener(); + _imageStream!.addListener(_getListener(recreateListener: true)); + _imageStream!.removeListener(oldListener); } if (widget.image != oldWidget.image) _resolveImage(); } @@ -84,31 +84,41 @@ class _TransitionImageState extends State { _updateSourceStream(newStream); } - ImageStreamListener _getListener() { - return ImageStreamListener( - _handleImageFrame, - onChunk: null, - ); + ImageStreamListener? _imageStreamListener; + + ImageStreamListener _getListener({bool recreateListener = false}) { + if (_imageStreamListener == null || recreateListener) { + _imageStreamListener = ImageStreamListener( + _handleImageFrame, + ); + } + return _imageStreamListener!; } void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) { setState(() { - _imageInfo = imageInfo; + _replaceImage(info: imageInfo); _frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1; }); } - // Updates _imageStream to newStream, and moves the stream listener - // registration from the old stream to the new stream (if a listener was - // registered). - void _updateSourceStream(ImageStream newStream) { - if (_imageStream?.key == newStream.key) return; + void _replaceImage({required ImageInfo? info}) { + _imageInfo?.dispose(); + _imageInfo = info; + } - if (_isListeningToStream) _imageStream!.removeListener(_getListener()); + void _updateSourceStream(ImageStream newStream) { + if (_imageStream?.key == newStream.key) { + return; + } + + if (_isListeningToStream) { + _imageStream!.removeListener(_getListener()); + } if (!widget.gaplessPlayback) { setState(() { - _imageInfo = null; + _replaceImage(info: null); }); } @@ -117,17 +127,26 @@ class _TransitionImageState extends State { }); _imageStream = newStream; - if (_isListeningToStream) _imageStream!.addListener(_getListener()); + if (_isListeningToStream) { + _imageStream!.addListener(_getListener()); + } } void _listenToStream() { - if (_isListeningToStream) return; + if (_isListeningToStream) { + return; + } + _imageStream!.addListener(_getListener()); + _isListeningToStream = true; } void _stopListeningToStream() { - if (!_isListeningToStream) return; + if (!_isListeningToStream) { + return; + } + _imageStream!.removeListener(_getListener()); _isListeningToStream = false; } @@ -141,6 +160,8 @@ class _TransitionImageState extends State { image: _imageInfo?.image, scale: _imageInfo?.scale ?? 1.0, t: t, + thumbnailFit: widget.thumbnailFit, + viewerFit: widget.viewerFit, background: widget.background, ), ), @@ -150,15 +171,17 @@ class _TransitionImageState extends State { class _TransitionImagePainter extends CustomPainter { final ui.Image? image; - final double scale; - final double t; + final double scale, t; final Color? background; + final BoxFit thumbnailFit, viewerFit; const _TransitionImagePainter({ required this.image, required this.scale, required this.t, - this.background, + required this.thumbnailFit, + required this.viewerFit, + required this.background, }); @override @@ -174,10 +197,10 @@ class _TransitionImagePainter extends CustomPainter { final inputSize = Size(image!.width.toDouble(), image!.height.toDouble()); final outputSize = rect.size; - final coverSizes = applyBoxFit(BoxFit.cover, inputSize / scale, size); - final containSizes = applyBoxFit(BoxFit.contain, inputSize / scale, size); - final sourceSize = Size.lerp(coverSizes.source, containSizes.source, t)! * scale; - final destinationSize = Size.lerp(coverSizes.destination, containSizes.destination, t)!; + final thumbnailSizes = applyBoxFit(thumbnailFit, inputSize / scale, size); + final viewerSizes = applyBoxFit(viewerFit, inputSize / scale, size); + final sourceSize = Size.lerp(thumbnailSizes.source, viewerSizes.source, t)! * scale; + final destinationSize = Size.lerp(thumbnailSizes.destination, viewerSizes.destination, t)!; final halfWidthDelta = (outputSize.width - destinationSize.width) / 2.0; final halfHeightDelta = (outputSize.height - destinationSize.height) / 2.0; diff --git a/lib/widgets/common/grid/draggable_thumb_label.dart b/lib/widgets/common/grid/draggable_thumb_label.dart index af6888457..eb516f34c 100644 --- a/lib/widgets/common/grid/draggable_thumb_label.dart +++ b/lib/widgets/common/grid/draggable_thumb_label.dart @@ -1,6 +1,6 @@ import 'package:aves/theme/format.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/grid/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/list_layout.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; @@ -41,10 +41,7 @@ class DraggableThumbLabel extends StatelessWidget { final sectionLayout = sll.getSectionAt(offsetY); if (sectionLayout == null) return const SizedBox(); - final section = sll.sections[sectionLayout.sectionKey]!; - final dy = offsetY - (sectionLayout.minOffset + sectionLayout.headerExtent); - final itemIndex = dy < 0 ? 0 : (dy ~/ (sll.tileHeight + sll.spacing)) * sll.columnCount; - final item = section[itemIndex]; + final item = sll.getItemAt(Offset(0, offsetY)) ?? sll.sections[sectionLayout.sectionKey]!.first; final lines = lineBuilder(context, item); if (lines.isEmpty) return const SizedBox(); diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart index 43137f3e2..14bcf5433 100644 --- a/lib/widgets/common/grid/header.dart +++ b/lib/widgets/common/grid/header.dart @@ -4,7 +4,7 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/grid/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/list_layout.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/grid/item_tracker.dart b/lib/widgets/common/grid/item_tracker.dart index abaa6e063..33e3d7472 100644 --- a/lib/widgets/common/grid/item_tracker.dart +++ b/lib/widgets/common/grid/item_tracker.dart @@ -4,7 +4,7 @@ import 'dart:math'; import 'package:aves/model/highlight.dart'; import 'package:aves/model/source/enums/enums.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/common/grid/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/list_layout.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/grid/overlay.dart b/lib/widgets/common/grid/overlay.dart index a7fc392de..a1ba82ad8 100644 --- a/lib/widgets/common/grid/overlay.dart +++ b/lib/widgets/common/grid/overlay.dart @@ -47,7 +47,7 @@ class GridItemSelectionOverlay extends StatelessWidget { alignment: AlignmentDirectional.topEnd, padding: padding, decoration: BoxDecoration( - color: isSelected ? Colors.black54 : Colors.transparent, + color: isSelected ? Theme.of(context).colorScheme.secondary.withOpacity(.6) : Colors.transparent, borderRadius: borderRadius, ), duration: duration, diff --git a/lib/widgets/common/grid/scaling.dart b/lib/widgets/common/grid/scaling.dart index 96988cbff..cb3bb8cbc 100644 --- a/lib/widgets/common/grid/scaling.dart +++ b/lib/widgets/common/grid/scaling.dart @@ -1,12 +1,10 @@ -import 'dart:ui' as ui; - import 'package:aves/model/highlight.dart'; import 'package:aves/model/source/enums/enums.dart'; -import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/grid/sections/fixed/scale_overlay.dart'; +import 'package:aves/widgets/common/grid/sections/mosaic/scale_overlay.dart'; +import 'package:aves/widgets/common/grid/sections/section_layout_builder.dart'; import 'package:aves/widgets/common/grid/theme.dart'; -import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:collection/collection.dart'; import 'package:flutter/gestures.dart'; @@ -26,7 +24,8 @@ class GridScaleGestureDetector extends StatefulWidget { final TileLayout tileLayout; final double Function(double width) heightForWidth; final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder; - final Widget Function(T item, Size tileSize) scaledBuilder; + final TileBuilder scaledItemBuilder; + final MosaicItemBuilder mosaicItemBuilder; final Object Function(T item)? highlightItem; final Widget child; @@ -36,7 +35,8 @@ class GridScaleGestureDetector extends StatefulWidget { required this.tileLayout, required this.heightForWidth, required this.gridBuilder, - required this.scaledBuilder, + required this.scaledItemBuilder, + required this.mosaicItemBuilder, this.highlightItem, required this.child, }); @@ -53,6 +53,8 @@ class _GridScaleGestureDetectorState extends State? _metadata; + TileLayout get tileLayout => widget.tileLayout; + @override Widget build(BuildContext context) { final gestureSettings = context.select((mq) => mq.gestureSettings); @@ -108,59 +110,66 @@ class _GridScaleGestureDetectorState extends State _ScaleOverlay( - builder: (scaledTileSize) { - late final double themeExtent; - switch (tileLayout) { - case TileLayout.grid: - themeExtent = scaledTileSize.width; - break; - case TileLayout.list: - themeExtent = scaledTileSize.height; - break; - } - return SizedBox.fromSize( - size: scaledTileSize, - child: GridTheme( - extent: themeExtent, - child: widget.scaledBuilder(_metadata!.item, scaledTileSize), + switch (tileLayout) { + case TileLayout.mosaic: + _overlayEntry = OverlayEntry( + builder: (context) => MosaicScaleOverlay( + contentRect: contentRect, + spacing: tileExtentController.spacing, + extentMax: _extentMax!, + scaledSizeNotifier: _scaledSizeNotifier!, + itemBuilder: widget.mosaicItemBuilder, + ), + ); + break; + case TileLayout.grid: + case TileLayout.list: + final tileCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height)); + _overlayEntry = OverlayEntry( + builder: (context) => FixedExtentScaleOverlay( + tileLayout: tileLayout, + tileCenter: tileCenter, + contentRect: contentRect, + scaledSizeNotifier: _scaledSizeNotifier!, + gridBuilder: widget.gridBuilder, + builder: (scaledTileSize) => SizedBox.fromSize( + size: scaledTileSize, + child: GridTheme( + extent: tileLayout == TileLayout.grid ? scaledTileSize.width : scaledTileSize.height, + child: widget.scaledItemBuilder(_metadata!.item, scaledTileSize), + ), ), - ); - }, - tileLayout: tileLayout, - center: tileCenter, - xMin: xMin, - xMax: xMax, - gridBuilder: widget.gridBuilder, - scaledSizeNotifier: _scaledSizeNotifier!, - ), - ); + ), + ); + break; + } Overlay.of(scrollableContext)!.insert(_overlayEntry!); } void _onScaleUpdate(ScaleUpdateDetails details) { if (_scaledSizeNotifier == null) return; final s = details.scale; - switch (widget.tileLayout) { + switch (tileLayout) { + case TileLayout.mosaic: case TileLayout.grid: final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!); _scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth)); @@ -184,7 +193,8 @@ class _GridScaleGestureDetectorState extends State extends State scaledSizeNotifier; - final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder; - - const _ScaleOverlay({ - required this.builder, - required this.tileLayout, - required this.center, - required this.xMin, - required this.xMax, - required this.scaledSizeNotifier, - required this.gridBuilder, - }); - - @override - State<_ScaleOverlay> createState() => _ScaleOverlayState(); -} - -class _ScaleOverlayState extends State<_ScaleOverlay> { - bool _init = false; - - Offset get center => widget.center; - - double get xMin => widget.xMin; - - double get xMax => widget.xMax; - - // `Color(0x00FFFFFF)` is different from `Color(0x00000000)` (or `Colors.transparent`) - // when used in gradients or lerping to it - static const transparentWhite = Color(0x00FFFFFF); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => _init = true)); - } - - @override - Widget build(BuildContext context) { - return MediaQueryDataProvider( - child: Builder( - builder: (context) => IgnorePointer( - child: AnimatedContainer( - decoration: _buildBackgroundDecoration(context), - duration: Durations.collectionScalingBackgroundAnimation, - child: ValueListenableBuilder( - valueListenable: widget.scaledSizeNotifier, - builder: (context, scaledSize, child) { - final width = scaledSize.width; - final height = scaledSize.height; - // keep scaled thumbnail within the screen - var dx = .0; - if (center.dx - width / 2 < xMin) { - dx = xMin - (center.dx - width / 2); - } else if (center.dx + width / 2 > xMax) { - dx = xMax - (center.dx + width / 2); - } - final clampedCenter = center.translate(dx, 0); - - var child = widget.builder(scaledSize); - child = Stack( - children: [ - Positioned( - left: clampedCenter.dx - width / 2, - top: clampedCenter.dy - height / 2, - child: DefaultTextStyle( - style: const TextStyle(), - child: child, - ), - ), - ], - ); - child = widget.gridBuilder(clampedCenter, scaledSize, child); - return child; - }, - ), - ), - ), - ), - ); - } - - BoxDecoration _buildBackgroundDecoration(BuildContext context) { - late final Offset gradientCenter; - switch (widget.tileLayout) { - case TileLayout.grid: - gradientCenter = center; - break; - case TileLayout.list: - gradientCenter = Offset(context.isRtl ? xMax : xMin, center.dy); - break; - } - - final isDark = Theme.of(context).brightness == Brightness.dark; - return _init - ? BoxDecoration( - gradient: RadialGradient( - center: FractionalOffset.fromOffsetAndSize(gradientCenter, context.select((mq) => mq.size)), - radius: 1, - colors: isDark - ? const [ - Colors.black, - Colors.black54, - ] - : const [ - Colors.white, - Colors.white38, - ], - ), - ) - : BoxDecoration( - // provide dummy gradient to lerp to the other one during animation - gradient: RadialGradient( - colors: isDark - ? const [ - Colors.transparent, - Colors.transparent, - ] - : const [ - transparentWhite, - transparentWhite, - ], - ), - ); - } -} - -class GridPainter extends CustomPainter { - final TileLayout tileLayout; - final Offset tileCenter; - final Size tileSize; - final double spacing, horizontalPadding, borderWidth; - final Radius borderRadius; - final Color color; - final TextDirection textDirection; - - const GridPainter({ - required this.tileLayout, - required this.tileCenter, - required this.tileSize, - required this.spacing, - required this.horizontalPadding, - required this.borderWidth, - required this.borderRadius, - required this.color, - required this.textDirection, - }); - - @override - void paint(Canvas canvas, Size size) { - late final Offset chipCenter; - late final Size chipSize; - late final int deltaColumn; - late final Shader strokeShader; - switch (tileLayout) { - case TileLayout.grid: - chipCenter = tileCenter; - chipSize = tileSize; - deltaColumn = 2; - strokeShader = ui.Gradient.radial( - tileCenter, - chipSize.shortestSide * 2, - [ - color, - Colors.transparent, - ], - [ - .8, - 1, - ], - ); - break; - case TileLayout.list: - chipSize = Size.square(tileSize.shortestSide); - final chipCenterToEdge = chipSize.width / 2; - chipCenter = Offset(textDirection == TextDirection.rtl ? size.width - (chipCenterToEdge + horizontalPadding) : chipCenterToEdge + horizontalPadding, tileCenter.dy); - deltaColumn = 0; - strokeShader = ui.Gradient.linear( - tileCenter - Offset(0, chipSize.shortestSide * 3), - tileCenter + Offset(0, chipSize.shortestSide * 3), - [ - Colors.transparent, - color, - color, - Colors.transparent, - ], - [ - 0, - .2, - .8, - 1, - ], - ); - break; - } - final strokePaint = Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = borderWidth - ..shader = strokeShader; - final fillPaint = Paint() - ..style = PaintingStyle.fill - ..color = color.withOpacity(.25); - - final chipWidth = chipSize.width; - final chipHeight = chipSize.height; - - final deltaX = tileSize.width + spacing; - final deltaY = tileSize.height + spacing; - for (var i = -deltaColumn; i <= deltaColumn; i++) { - final dx = deltaX * i; - for (var j = -2; j <= 2; j++) { - if (i == 0 && j == 0) continue; - final dy = deltaY * j; - final rect = RRect.fromRectAndRadius( - Rect.fromCenter( - center: chipCenter + Offset(dx, dy), - width: chipWidth - borderWidth, - height: chipHeight - borderWidth, - ), - borderRadius, - ); - - if ((i.abs() == 1 && j == 0) || (j.abs() == 1 && i == 0)) { - canvas.drawRRect(rect, fillPaint); - } - canvas.drawRRect(rect, strokePaint); - } - } - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => true; -} diff --git a/lib/widgets/common/grid/section_layout.dart b/lib/widgets/common/grid/section_layout.dart deleted file mode 100644 index 040304d67..000000000 --- a/lib/widgets/common/grid/section_layout.dart +++ /dev/null @@ -1,450 +0,0 @@ -import 'dart:math'; - -import 'package:aves/model/source/enums/enums.dart'; -import 'package:aves/model/source/section_keys.dart'; -import 'package:aves/theme/durations.dart'; -import 'package:collection/collection.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:provider/provider.dart'; - -abstract class SectionedListLayoutProvider extends StatelessWidget { - final double scrollableWidth; - final TileLayout tileLayout; - final int columnCount; - final double spacing, horizontalPadding, tileWidth, tileHeight; - final Widget Function(T item) tileBuilder; - final Duration tileAnimationDelay; - final Widget child; - - const SectionedListLayoutProvider({ - super.key, - required this.scrollableWidth, - required this.tileLayout, - required int columnCount, - required this.spacing, - required this.horizontalPadding, - required double tileWidth, - required this.tileHeight, - required this.tileBuilder, - required this.tileAnimationDelay, - required this.child, - }) : assert(scrollableWidth != 0), - columnCount = tileLayout == TileLayout.list ? 1 : columnCount, - tileWidth = tileLayout == TileLayout.list ? scrollableWidth - (horizontalPadding * 2) : tileWidth; - - @override - Widget build(BuildContext context) { - return ProxyProvider0>( - update: (context, _) => _updateLayouts(context), - child: child, - ); - } - - SectionedListLayout _updateLayouts(BuildContext context) { - final _showHeaders = showHeaders; - final _sections = sections; - final sectionKeys = _sections.keys.toList(); - final animate = tileAnimationDelay > Duration.zero; - - final sectionLayouts = []; - var currentIndex = 0; - var currentOffset = 0.0; - sectionKeys.forEach((sectionKey) { - final section = _sections[sectionKey]!; - final sectionItemCount = section.length; - final rowCount = (sectionItemCount / columnCount).ceil(); - final sectionChildCount = 1 + rowCount; - - final headerExtent = _showHeaders ? getHeaderExtent(context, sectionKey) : 0.0; - - final sectionFirstIndex = currentIndex; - currentIndex += sectionChildCount; - final sectionLastIndex = currentIndex - 1; - - final sectionMinOffset = currentOffset; - currentOffset += headerExtent + tileHeight * rowCount + spacing * (rowCount - 1); - final sectionMaxOffset = currentOffset; - - sectionLayouts.add( - SectionLayout( - sectionKey: sectionKey, - firstIndex: sectionFirstIndex, - lastIndex: sectionLastIndex, - minOffset: sectionMinOffset, - maxOffset: sectionMaxOffset, - headerExtent: headerExtent, - tileHeight: tileHeight, - spacing: spacing, - builder: (context, listIndex) => _buildInSection( - context, - section, - listIndex * columnCount, - listIndex - sectionFirstIndex, - sectionKey, - headerExtent, - animate, - ), - ), - ); - }); - return SectionedListLayout( - sections: _sections, - showHeaders: _showHeaders, - columnCount: columnCount, - tileWidth: tileWidth, - tileHeight: tileHeight, - spacing: spacing, - horizontalPadding: horizontalPadding, - sectionLayouts: sectionLayouts, - ); - } - - Widget _buildInSection( - BuildContext context, - List section, - int sectionGridIndex, - int sectionChildIndex, - SectionKey sectionKey, - double headerExtent, - bool animate, - ) { - if (sectionChildIndex == 0) { - final header = headerExtent > 0 ? buildHeader(context, sectionKey, headerExtent) : const SizedBox.shrink(); - return animate ? _buildAnimation(context, sectionGridIndex, header) : header; - } - sectionChildIndex--; - - final sectionItemCount = section.length; - - final minItemIndex = sectionChildIndex * columnCount; - final maxItemIndex = min(sectionItemCount, minItemIndex + columnCount); - final children = []; - for (var i = minItemIndex; i < maxItemIndex; i++) { - final itemGridIndex = sectionGridIndex + i - minItemIndex; - final item = RepaintBoundary( - child: tileBuilder(section[i]), - ); - children.add(animate ? _buildAnimation(context, itemGridIndex, item) : item); - } - return Padding( - padding: EdgeInsets.symmetric(horizontal: horizontalPadding), - child: _GridRow( - width: tileWidth, - height: tileHeight, - spacing: spacing, - textDirection: Directionality.of(context), - children: children, - ), - ); - } - - Widget _buildAnimation(BuildContext context, int index, Widget child) { - final durations = context.watch(); - return AnimationConfiguration.staggeredGrid( - position: index, - columnCount: columnCount, - duration: durations.staggeredAnimation, - delay: tileAnimationDelay, - child: SlideAnimation( - verticalOffset: 50.0, - child: FadeInAnimation( - child: child, - ), - ), - ); - } - - bool get showHeaders; - - Map> get sections; - - double getHeaderExtent(BuildContext context, SectionKey sectionKey); - - Widget buildHeader(BuildContext context, SectionKey sectionKey, double headerExtent); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DoubleProperty('scrollableWidth', scrollableWidth)); - properties.add(IntProperty('columnCount', columnCount)); - properties.add(DoubleProperty('spacing', spacing)); - properties.add(DoubleProperty('horizontalPadding', horizontalPadding)); - properties.add(DoubleProperty('tileWidth', tileWidth)); - properties.add(DoubleProperty('tileHeight', tileHeight)); - properties.add(DiagnosticsProperty('showHeaders', showHeaders)); - } -} - -class SectionedListLayout { - final Map> sections; - final bool showHeaders; - final int columnCount; - final double tileWidth, tileHeight, spacing, horizontalPadding; - final List sectionLayouts; - - const SectionedListLayout({ - required this.sections, - required this.showHeaders, - required this.columnCount, - required this.tileWidth, - required this.tileHeight, - required this.spacing, - required this.horizontalPadding, - required this.sectionLayouts, - }); - - // return tile rectangle in layout space, i.e. x=0 is start - Rect? getTileRect(T item) { - final MapEntry>? section = sections.entries.firstWhereOrNull((kv) => kv.value.contains(item)); - if (section == null) return null; - - final sectionKey = section.key; - final sectionLayout = sectionLayouts.firstWhereOrNull((sl) => sl.sectionKey == sectionKey); - if (sectionLayout == null) return null; - - final sectionItemIndex = section.value.indexOf(item); - final column = sectionItemIndex % columnCount; - final row = (sectionItemIndex / columnCount).floor(); - final listIndex = sectionLayout.firstIndex + 1 + row; - - final left = horizontalPadding + tileWidth * column + spacing * (column - 1); - final top = sectionLayout.indexToLayoutOffset(listIndex); - return Rect.fromLTWH(left, top, tileWidth, tileHeight); - } - - SectionLayout? getSectionAt(double offsetY) => sectionLayouts.firstWhereOrNull((sl) => offsetY < sl.maxOffset); - - // `position` in layout space, i.e. x=0 is start - T? getItemAt(Offset position) { - var dy = position.dy; - final sectionLayout = getSectionAt(dy); - if (sectionLayout == null) return null; - - final section = sections[sectionLayout.sectionKey]; - if (section == null) return null; - - dy -= sectionLayout.minOffset + sectionLayout.headerExtent; - if (dy < 0) return null; - - final row = dy ~/ (tileHeight + spacing); - final column = max(0, position.dx - horizontalPadding) ~/ (tileWidth + spacing); - final index = row * columnCount + column; - if (index >= section.length) return null; - - return section[index]; - } - - @override - String toString() => '$runtimeType#${shortHash(this)}{sectionCount=${sections.length} columnCount=$columnCount, tileWidth=$tileWidth, tileHeight=$tileHeight}'; -} - -@immutable -class SectionLayout extends Equatable { - final SectionKey sectionKey; - final int firstIndex, lastIndex, bodyFirstIndex; - final double minOffset, maxOffset, bodyMinOffset; - final double headerExtent, tileHeight, spacing, mainAxisStride; - final IndexedWidgetBuilder builder; - - @override - List get props => [sectionKey, firstIndex, lastIndex, minOffset, maxOffset, headerExtent, tileHeight, spacing]; - - const SectionLayout({ - required this.sectionKey, - required this.firstIndex, - required this.lastIndex, - required this.minOffset, - required this.maxOffset, - required this.headerExtent, - required this.tileHeight, - required this.spacing, - required this.builder, - }) : bodyFirstIndex = firstIndex + 1, - bodyMinOffset = minOffset + headerExtent, - mainAxisStride = tileHeight + spacing; - - bool hasChild(int index) => firstIndex <= index && index <= lastIndex; - - bool hasChildAtOffset(double scrollOffset) => minOffset <= scrollOffset && scrollOffset <= maxOffset; - - double indexToLayoutOffset(int index) { - index -= bodyFirstIndex; - if (index < 0) return minOffset; - return bodyMinOffset + index * mainAxisStride; - } - - int getMinChildIndexForScrollOffset(double scrollOffset) { - scrollOffset -= bodyMinOffset; - if (scrollOffset < 0) return firstIndex; - return bodyFirstIndex + scrollOffset ~/ mainAxisStride; - } - - int getMaxChildIndexForScrollOffset(double scrollOffset) { - scrollOffset -= bodyMinOffset; - if (scrollOffset < 0) return firstIndex; - return bodyFirstIndex + (scrollOffset / mainAxisStride).ceil() - 1; - } -} - -class _GridRow extends MultiChildRenderObjectWidget { - final double width, height, spacing; - final TextDirection textDirection; - - _GridRow({ - required this.width, - required this.height, - required this.spacing, - required this.textDirection, - required List children, - }) : super(children: children); - - @override - RenderObject createRenderObject(BuildContext context) { - return _RenderGridRow( - width: width, - height: height, - spacing: spacing, - textDirection: textDirection, - ); - } - - @override - void updateRenderObject(BuildContext context, _RenderGridRow renderObject) { - renderObject.width = width; - renderObject.height = height; - renderObject.spacing = spacing; - renderObject.textDirection = textDirection; - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DoubleProperty('width', width)); - properties.add(DoubleProperty('height', height)); - properties.add(DoubleProperty('spacing', spacing)); - properties.add(EnumProperty('textDirection', textDirection)); - } -} - -class _GridRowParentData extends ContainerBoxParentData {} - -class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { - _RenderGridRow({ - List? children, - required double width, - required double height, - required double spacing, - required TextDirection textDirection, - }) : _width = width, - _height = height, - _spacing = spacing, - _textDirection = textDirection { - addAll(children); - } - - double get width => _width; - double _width; - - set width(double value) { - if (_width == value) return; - _width = value; - markNeedsLayout(); - } - - double get height => _height; - double _height; - - set height(double value) { - if (_height == value) return; - _height = value; - markNeedsLayout(); - } - - double get spacing => _spacing; - double _spacing; - - set spacing(double value) { - if (_spacing == value) return; - _spacing = value; - markNeedsLayout(); - } - - TextDirection get textDirection => _textDirection; - TextDirection _textDirection; - - set textDirection(TextDirection value) { - if (_textDirection == value) return; - _textDirection = value; - markNeedsLayout(); - } - - @override - void setupParentData(RenderBox child) { - if (child.parentData is! _GridRowParentData) { - child.parentData = _GridRowParentData(); - } - } - - double get intrinsicWidth => width * childCount + spacing * (childCount - 1); - - @override - double computeMinIntrinsicWidth(double height) => intrinsicWidth; - - @override - double computeMaxIntrinsicWidth(double height) => intrinsicWidth; - - @override - double computeMinIntrinsicHeight(double width) => height; - - @override - double computeMaxIntrinsicHeight(double width) => height; - - @override - void performLayout() { - var child = firstChild; - if (child == null) { - size = constraints.smallest; - return; - } - size = Size(constraints.maxWidth, height); - final childConstraints = BoxConstraints.tight(Size(width, height)); - final flipMainAxis = textDirection == TextDirection.rtl; - var offset = Offset(flipMainAxis ? size.width - width : 0, 0); - final dx = (flipMainAxis ? -1 : 1) * (width + spacing); - while (child != null) { - child.layout(childConstraints, parentUsesSize: false); - final childParentData = child.parentData! as _GridRowParentData; - childParentData.offset = offset; - offset += Offset(dx, 0); - child = childParentData.nextSibling; - } - } - - @override - double? computeDistanceToActualBaseline(TextBaseline baseline) { - return defaultComputeDistanceToHighestActualBaseline(baseline); - } - - @override - bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { - return defaultHitTestChildren(result, position: position); - } - - @override - void paint(PaintingContext context, Offset offset) { - defaultPaint(context, offset); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DoubleProperty('width', width)); - properties.add(DoubleProperty('height', height)); - properties.add(DoubleProperty('spacing', spacing)); - properties.add(EnumProperty('textDirection', textDirection)); - } -} diff --git a/lib/widgets/common/grid/sections/fixed/list_layout.dart b/lib/widgets/common/grid/sections/fixed/list_layout.dart new file mode 100644 index 000000000..162fcc89c --- /dev/null +++ b/lib/widgets/common/grid/sections/fixed/list_layout.dart @@ -0,0 +1,62 @@ +import 'dart:math'; + +import 'package:aves/model/source/section_keys.dart'; +import 'package:aves/widgets/common/grid/sections/list_layout.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class FixedExtentSectionedListLayout extends SectionedListLayout { + final int columnCount; + final double tileWidth, tileHeight; + + const FixedExtentSectionedListLayout({ + required super.sections, + required super.showHeaders, + required this.columnCount, + required this.tileWidth, + required this.tileHeight, + required super.spacing, + required super.horizontalPadding, + required super.sectionLayouts, + }); + + @override + Rect? getTileRect(T item) { + final MapEntry>? section = sections.entries.firstWhereOrNull((kv) => kv.value.contains(item)); + if (section == null) return null; + + final sectionKey = section.key; + final sectionLayout = sectionLayouts.firstWhereOrNull((sl) => sl.sectionKey == sectionKey); + if (sectionLayout == null) return null; + + final sectionItemIndex = section.value.indexOf(item); + final column = sectionItemIndex % columnCount; + final row = (sectionItemIndex / columnCount).floor(); + final listIndex = sectionLayout.firstIndex + 1 + row; + + final left = horizontalPadding + tileWidth * column + spacing * (column - 1); + final top = sectionLayout.indexToLayoutOffset(listIndex); + return Rect.fromLTWH(left, top, tileWidth, tileHeight); + } + + @override + T? getItemAt(Offset position) { + var dy = position.dy; + final sectionLayout = getSectionAt(dy); + if (sectionLayout == null) return null; + + final section = sections[sectionLayout.sectionKey]; + if (section == null) return null; + + dy -= sectionLayout.minOffset + sectionLayout.headerExtent; + if (dy < 0) return null; + + final row = dy ~/ (tileHeight + spacing); + final column = max(0, position.dx - horizontalPadding) ~/ (tileWidth + spacing); + final index = row * columnCount + column; + if (index >= section.length) return null; + + return section[index]; + } +} diff --git a/lib/widgets/common/grid/sections/fixed/row.dart b/lib/widgets/common/grid/sections/fixed/row.dart new file mode 100644 index 000000000..e490246a2 --- /dev/null +++ b/lib/widgets/common/grid/sections/fixed/row.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class FixedExtentGridRow extends MultiChildRenderObjectWidget { + final double width, height, spacing; + final TextDirection textDirection; + + FixedExtentGridRow({ + super.key, + required this.width, + required this.height, + required this.spacing, + required this.textDirection, + required List children, + }) : super(children: children); + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderFixedExtentGridRow( + width: width, + height: height, + spacing: spacing, + textDirection: textDirection, + ); + } + + @override + void updateRenderObject(BuildContext context, RenderFixedExtentGridRow renderObject) { + renderObject.width = width; + renderObject.height = height; + renderObject.spacing = spacing; + renderObject.textDirection = textDirection; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('width', width)); + properties.add(DoubleProperty('height', height)); + properties.add(DoubleProperty('spacing', spacing)); + properties.add(EnumProperty('textDirection', textDirection)); + } +} + +class _GridRowParentData extends ContainerBoxParentData {} + +class RenderFixedExtentGridRow extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { + RenderFixedExtentGridRow({ + List? children, + required double width, + required double height, + required double spacing, + required TextDirection textDirection, + }) : _width = width, + _height = height, + _spacing = spacing, + _textDirection = textDirection { + addAll(children); + } + + double get width => _width; + double _width; + + set width(double value) { + if (_width == value) return; + _width = value; + markNeedsLayout(); + } + + double get height => _height; + double _height; + + set height(double value) { + if (_height == value) return; + _height = value; + markNeedsLayout(); + } + + double get spacing => _spacing; + double _spacing; + + set spacing(double value) { + if (_spacing == value) return; + _spacing = value; + markNeedsLayout(); + } + + TextDirection get textDirection => _textDirection; + TextDirection _textDirection; + + set textDirection(TextDirection value) { + if (_textDirection == value) return; + _textDirection = value; + markNeedsLayout(); + } + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! _GridRowParentData) { + child.parentData = _GridRowParentData(); + } + } + + double get intrinsicWidth => width * childCount + spacing * (childCount - 1); + + @override + double computeMinIntrinsicWidth(double height) => intrinsicWidth; + + @override + double computeMaxIntrinsicWidth(double height) => intrinsicWidth; + + @override + double computeMinIntrinsicHeight(double width) => height; + + @override + double computeMaxIntrinsicHeight(double width) => height; + + @override + void performLayout() { + var child = firstChild; + if (child == null) { + size = constraints.smallest; + return; + } + size = Size(constraints.maxWidth, height); + final childConstraints = BoxConstraints.tight(Size(width, height)); + final flipMainAxis = textDirection == TextDirection.rtl; + var offset = Offset(flipMainAxis ? size.width - width : 0, 0); + final dx = (flipMainAxis ? -1 : 1) * (width + spacing); + while (child != null) { + child.layout(childConstraints, parentUsesSize: false); + final childParentData = child.parentData! as _GridRowParentData; + childParentData.offset = offset; + offset += Offset(dx, 0); + child = childParentData.nextSibling; + } + } + + @override + double? computeDistanceToActualBaseline(TextBaseline baseline) { + return defaultComputeDistanceToHighestActualBaseline(baseline); + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + return defaultHitTestChildren(result, position: position); + } + + @override + void paint(PaintingContext context, Offset offset) { + defaultPaint(context, offset); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('width', width)); + properties.add(DoubleProperty('height', height)); + properties.add(DoubleProperty('spacing', spacing)); + properties.add(EnumProperty('textDirection', textDirection)); + } +} diff --git a/lib/widgets/common/grid/sections/fixed/scale_grid.dart b/lib/widgets/common/grid/sections/fixed/scale_grid.dart new file mode 100644 index 000000000..9f9d869fd --- /dev/null +++ b/lib/widgets/common/grid/sections/fixed/scale_grid.dart @@ -0,0 +1,113 @@ +import 'dart:ui' as ui; + +import 'package:aves/model/source/enums/enums.dart'; +import 'package:flutter/material.dart'; + +class FixedExtentGridPainter extends CustomPainter { + final TileLayout tileLayout; + final Offset tileCenter; + final Size tileSize; + final double spacing, horizontalPadding, borderWidth; + final Radius borderRadius; + final Color color; + final TextDirection textDirection; + + const FixedExtentGridPainter({ + required this.tileLayout, + required this.tileCenter, + required this.tileSize, + required this.spacing, + required this.horizontalPadding, + required this.borderWidth, + required this.borderRadius, + required this.color, + required this.textDirection, + }); + + @override + void paint(Canvas canvas, Size size) { + late final Offset chipCenter; + late final Size chipSize; + late final int deltaColumn; + late final Shader strokeShader; + switch (tileLayout) { + case TileLayout.mosaic: + return; + case TileLayout.grid: + chipCenter = tileCenter; + chipSize = tileSize; + deltaColumn = 2; + strokeShader = ui.Gradient.radial( + tileCenter, + chipSize.shortestSide * 2, + [ + color, + Colors.transparent, + ], + [ + .8, + 1, + ], + ); + break; + case TileLayout.list: + chipSize = Size.square(tileSize.shortestSide); + final chipCenterToEdge = chipSize.width / 2; + chipCenter = Offset(textDirection == TextDirection.rtl ? size.width - (chipCenterToEdge + horizontalPadding) : chipCenterToEdge + horizontalPadding, tileCenter.dy); + deltaColumn = 0; + strokeShader = ui.Gradient.linear( + tileCenter - Offset(0, chipSize.shortestSide * 3), + tileCenter + Offset(0, chipSize.shortestSide * 3), + [ + Colors.transparent, + color, + color, + Colors.transparent, + ], + [ + 0, + .2, + .8, + 1, + ], + ); + break; + } + final strokePaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = borderWidth + ..shader = strokeShader; + final fillPaint = Paint() + ..style = PaintingStyle.fill + ..color = color.withOpacity(.25); + + final chipWidth = chipSize.width; + final chipHeight = chipSize.height; + + final deltaX = tileSize.width + spacing; + final deltaY = tileSize.height + spacing; + for (var i = -deltaColumn; i <= deltaColumn; i++) { + final dx = deltaX * i; + for (var j = -2; j <= 2; j++) { + if (i == 0 && j == 0) continue; + final dy = deltaY * j; + final rect = RRect.fromRectAndRadius( + Rect.fromCenter( + center: chipCenter + Offset(dx, dy), + width: chipWidth - borderWidth, + height: chipHeight - borderWidth, + ), + borderRadius, + ); + + if ((i.abs() == 1 && j == 0) || (j.abs() == 1 && i == 0)) { + canvas.drawRRect(rect, fillPaint); + } + canvas.drawRRect(rect, strokePaint); + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/lib/widgets/common/grid/sections/fixed/scale_overlay.dart b/lib/widgets/common/grid/sections/fixed/scale_overlay.dart new file mode 100644 index 000000000..4bae5d50e --- /dev/null +++ b/lib/widgets/common/grid/sections/fixed/scale_overlay.dart @@ -0,0 +1,136 @@ +import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class FixedExtentScaleOverlay extends StatelessWidget { + final TileLayout tileLayout; + final Offset tileCenter; + final double xMin, xMax; + final ValueNotifier scaledSizeNotifier; + final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder; + final Widget Function(Size scaledTileSize) builder; + + FixedExtentScaleOverlay({ + super.key, + required this.tileLayout, + required this.tileCenter, + required Rect contentRect, + required this.scaledSizeNotifier, + required this.gridBuilder, + required this.builder, + }) : xMin = contentRect.left, + xMax = contentRect.right; + + @override + Widget build(BuildContext context) { + return MediaQueryDataProvider( + child: IgnorePointer( + child: _OverlayBackground( + gradientCenter: tileLayout == TileLayout.grid ? tileCenter : Offset(context.isRtl ? xMax : xMin, tileCenter.dy), + child: ValueListenableBuilder( + valueListenable: scaledSizeNotifier, + builder: (context, scaledSize, child) { + final width = scaledSize.width; + final height = scaledSize.height; + // keep scaled thumbnail within the screen + var dx = .0; + if (tileCenter.dx - width / 2 < xMin) { + dx = xMin - (tileCenter.dx - width / 2); + } else if (tileCenter.dx + width / 2 > xMax) { + dx = xMax - (tileCenter.dx + width / 2); + } + final clampedCenter = tileCenter.translate(dx, 0); + + var child = builder(scaledSize); + child = Stack( + children: [ + Positioned( + left: clampedCenter.dx - width / 2, + top: clampedCenter.dy - height / 2, + child: DefaultTextStyle( + style: const TextStyle(), + child: child, + ), + ), + ], + ); + child = gridBuilder(clampedCenter, scaledSize, child); + return child; + }, + ), + ), + ), + ); + } +} + +class _OverlayBackground extends StatefulWidget { + final Offset gradientCenter; + final Widget child; + + const _OverlayBackground({ + required this.gradientCenter, + required this.child, + }); + + @override + State<_OverlayBackground> createState() => _OverlayBackgroundState(); +} + +class _OverlayBackgroundState extends State<_OverlayBackground> { + bool _initialized = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => _initialized = true)); + } + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + decoration: _buildBackgroundDecoration(context), + duration: Durations.scalingGridBackgroundAnimation, + child: widget.child, + ); + } + + BoxDecoration _buildBackgroundDecoration(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final gradientCenter = widget.gradientCenter; + return _initialized + ? BoxDecoration( + gradient: RadialGradient( + center: FractionalOffset.fromOffsetAndSize(gradientCenter, context.select((mq) => mq.size)), + radius: 1, + colors: isDark + ? const [ + Colors.black, + Colors.black54, + ] + : const [ + Colors.white, + Colors.white38, + ], + ), + ) + : BoxDecoration( + // provide dummy gradient to lerp to the other one during animation + gradient: RadialGradient( + colors: isDark + ? const [ + Constants.transparentBlack, + Constants.transparentBlack, + ] + : const [ + Constants.transparentWhite, + Constants.transparentWhite, + ], + ), + ); + } +} diff --git a/lib/widgets/common/grid/sections/fixed/section_layout.dart b/lib/widgets/common/grid/sections/fixed/section_layout.dart new file mode 100644 index 000000000..2d29be8e0 --- /dev/null +++ b/lib/widgets/common/grid/sections/fixed/section_layout.dart @@ -0,0 +1,41 @@ +import 'package:aves/widgets/common/grid/sections/section_layout.dart'; + +class FixedExtentSectionLayout extends SectionLayout { + final double tileHeight, mainAxisStride; + + @override + List get props => [sectionKey, firstIndex, lastIndex, minOffset, maxOffset, headerExtent, tileHeight, spacing]; + + const FixedExtentSectionLayout({ + required super.sectionKey, + required super.firstIndex, + required super.lastIndex, + required super.minOffset, + required super.maxOffset, + required super.headerExtent, + required this.tileHeight, + required super.spacing, + required super.builder, + }) : mainAxisStride = tileHeight + spacing; + + @override + double indexToLayoutOffset(int index) { + index -= bodyFirstIndex; + if (index < 0) return minOffset; + return bodyMinOffset + index * mainAxisStride; + } + + @override + int getMinChildIndexForScrollOffset(double scrollOffset) { + scrollOffset -= bodyMinOffset; + if (scrollOffset < 0) return firstIndex; + return bodyFirstIndex + scrollOffset ~/ mainAxisStride; + } + + @override + int getMaxChildIndexForScrollOffset(double scrollOffset) { + scrollOffset -= bodyMinOffset; + if (scrollOffset < 0) return firstIndex; + return bodyFirstIndex + (scrollOffset / mainAxisStride).ceil() - 1; + } +} diff --git a/lib/widgets/common/grid/sections/fixed/section_layout_builder.dart b/lib/widgets/common/grid/sections/fixed/section_layout_builder.dart new file mode 100644 index 000000000..accf3d9d3 --- /dev/null +++ b/lib/widgets/common/grid/sections/fixed/section_layout_builder.dart @@ -0,0 +1,110 @@ +import 'package:aves/model/source/section_keys.dart'; +import 'package:aves/widgets/common/grid/sections/fixed/list_layout.dart'; +import 'package:aves/widgets/common/grid/sections/fixed/row.dart'; +import 'package:aves/widgets/common/grid/sections/fixed/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/list_layout.dart'; +import 'package:aves/widgets/common/grid/sections/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/section_layout_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:tuple/tuple.dart'; + +class FixedExtentSectionLayoutBuilder extends SectionLayoutBuilder { + int _currentIndex = 0; + double _currentOffset = 0; + final List _itemSizes; + + FixedExtentSectionLayoutBuilder({ + required super.sections, + required super.showHeaders, + required super.getHeaderExtent, + required super.buildHeader, + required super.scrollableWidth, + required super.tileLayout, + required super.columnCount, + required super.spacing, + required super.horizontalPadding, + required super.tileWidth, + required super.tileHeight, + required super.tileBuilder, + required super.tileAnimationDelay, + }) : _itemSizes = List.generate(columnCount, (index) => Size(tileWidth, tileHeight)); + + @override + SectionedListLayout updateLayouts(BuildContext context) { + final sectionLayouts = sections.keys + .map((sectionKey) => buildSectionLayout( + headerExtent: showHeaders ? getHeaderExtent(context, sectionKey) : 0.0, + sectionKey: sectionKey, + section: sections[sectionKey]!, + animate: animate, + )) + .toList(); + + return FixedExtentSectionedListLayout( + sections: sections, + showHeaders: showHeaders, + columnCount: columnCount, + tileWidth: tileWidth, + tileHeight: tileHeight, + spacing: spacing, + horizontalPadding: horizontalPadding, + sectionLayouts: sectionLayouts, + ); + } + + @override + SectionLayout buildSectionLayout({ + required double headerExtent, + required SectionKey sectionKey, + required List section, + required bool animate, + }) { + final sectionItemCount = section.length; + final rowCount = (sectionItemCount / columnCount).ceil(); + final sectionChildCount = 1 + rowCount; + + final sectionFirstIndex = _currentIndex; + _currentIndex += sectionChildCount; + final sectionLastIndex = _currentIndex - 1; + + final sectionMinOffset = _currentOffset; + _currentOffset += headerExtent + tileHeight * rowCount + spacing * (rowCount - 1); + final sectionMaxOffset = _currentOffset; + + return FixedExtentSectionLayout( + sectionKey: sectionKey, + firstIndex: sectionFirstIndex, + lastIndex: sectionLastIndex, + minOffset: sectionMinOffset, + maxOffset: sectionMaxOffset, + headerExtent: headerExtent, + tileHeight: tileHeight, + spacing: spacing, + builder: (context, listIndex) { + final textDirection = Directionality.of(context); + final sectionChildIndex = listIndex - sectionFirstIndex; + return buildSectionWidget( + context: context, + section: section, + sectionGridIndex: listIndex * columnCount, + sectionChildIndex: sectionChildIndex, + itemIndexRange: () => Tuple2( + (sectionChildIndex - 1) * columnCount, + sectionChildIndex * columnCount, + ), + sectionKey: sectionKey, + headerExtent: headerExtent, + itemSizes: _itemSizes, + animate: animate, + buildGridRow: (children) => FixedExtentGridRow( + width: tileWidth, + height: tileHeight, + spacing: spacing, + textDirection: textDirection, + children: children, + ), + ); + }, + ); + } +} diff --git a/lib/widgets/common/grid/sections/list_layout.dart b/lib/widgets/common/grid/sections/list_layout.dart new file mode 100644 index 000000000..aac861bb8 --- /dev/null +++ b/lib/widgets/common/grid/sections/list_layout.dart @@ -0,0 +1,28 @@ +import 'package:aves/model/source/section_keys.dart'; +import 'package:aves/widgets/common/grid/sections/section_layout.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +abstract class SectionedListLayout { + final Map> sections; + final bool showHeaders; + final double spacing, horizontalPadding; + final List sectionLayouts; + + const SectionedListLayout({ + required this.sections, + required this.showHeaders, + required this.spacing, + required this.horizontalPadding, + required this.sectionLayouts, + }); + + // return tile rectangle in layout space, i.e. x=0 is start + Rect? getTileRect(T item); + + SectionLayout? getSectionAt(double offsetY) => sectionLayouts.firstWhereOrNull((sl) => offsetY < sl.maxOffset); + + // `position` in layout space, i.e. x=0 is start + T? getItemAt(Offset position); +} diff --git a/lib/widgets/common/grid/sections/mosaic/list_layout.dart b/lib/widgets/common/grid/sections/mosaic/list_layout.dart new file mode 100644 index 000000000..4590bf91b --- /dev/null +++ b/lib/widgets/common/grid/sections/mosaic/list_layout.dart @@ -0,0 +1,78 @@ +import 'package:aves/model/source/section_keys.dart'; +import 'package:aves/widgets/common/grid/sections/list_layout.dart'; +import 'package:aves/widgets/common/grid/sections/mosaic/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/section_layout.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class MosaicSectionedListLayout extends SectionedListLayout { + const MosaicSectionedListLayout({ + required super.sections, + required super.showHeaders, + required super.spacing, + required super.horizontalPadding, + required super.sectionLayouts, + }); + + List _rowsFor(SectionLayout sectionLayout) => (sectionLayout as MosaicSectionLayout).rows; + + @override + Rect? getTileRect(T item) { + final MapEntry>? section = sections.entries.firstWhereOrNull((kv) => kv.value.contains(item)); + if (section == null) return null; + + final sectionKey = section.key; + final sectionLayout = sectionLayouts.firstWhereOrNull((sl) => sl.sectionKey == sectionKey); + if (sectionLayout == null) return null; + + final sectionItemIndex = section.value.indexOf(item); + final row = _rowsFor(sectionLayout).firstWhereOrNull((row) => sectionItemIndex <= row.lastIndex); + if (row == null) return null; + + final rowItemIndex = sectionItemIndex - row.firstIndex; + final tileWidth = row.itemWidths[rowItemIndex]; + final tileHeight = row.height - spacing; + + var left = horizontalPadding; + row.itemWidths.forEachIndexedWhile((i, width) { + if (i == rowItemIndex) return true; + + left += width + spacing; + return false; + }); + final listIndex = sectionLayout.firstIndex + 1 + _rowsFor(sectionLayout).indexOf(row); + + final top = sectionLayout.indexToLayoutOffset(listIndex); + return Rect.fromLTWH(left, top, tileWidth, tileHeight); + } + + @override + T? getItemAt(Offset position) { + var dy = position.dy; + final sectionLayout = getSectionAt(dy); + if (sectionLayout == null) return null; + + final section = sections[sectionLayout.sectionKey]; + if (section == null) return null; + + dy -= sectionLayout.minOffset + sectionLayout.headerExtent; + if (dy < 0) return null; + + final row = _rowsFor(sectionLayout).firstWhereOrNull((v) => dy < v.maxOffset); + if (row == null) return null; + + var dx = position.dx - horizontalPadding; + var index = -1; + row.itemWidths.forEachIndexedWhile((i, width) { + dx -= width + spacing; + if (dx > 0) return true; + + index = row.firstIndex + i; + return false; + }); + + if (index < 0 || index >= section.length) return null; + return section[index]; + } +} diff --git a/lib/widgets/common/grid/sections/mosaic/row.dart b/lib/widgets/common/grid/sections/mosaic/row.dart new file mode 100644 index 000000000..4d0767f96 --- /dev/null +++ b/lib/widgets/common/grid/sections/mosaic/row.dart @@ -0,0 +1,154 @@ +import 'package:aves/widgets/common/grid/sections/mosaic/section_layout.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class MosaicGridRow extends MultiChildRenderObjectWidget { + final MosaicRowLayout rowLayout; + final double spacing; + final TextDirection textDirection; + + MosaicGridRow({ + super.key, + required this.rowLayout, + required this.spacing, + required this.textDirection, + required List children, + }) : super(children: children); + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderMosaicGridRow( + rowLayout: rowLayout, + spacing: spacing, + textDirection: textDirection, + ); + } + + @override + void updateRenderObject(BuildContext context, RenderMosaicGridRow renderObject) { + renderObject.rowLayout = rowLayout; + renderObject.spacing = spacing; + renderObject.textDirection = textDirection; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('rowLayout', rowLayout)); + properties.add(DoubleProperty('spacing', spacing)); + properties.add(EnumProperty('textDirection', textDirection)); + } +} + +class _GridRowParentData extends ContainerBoxParentData {} + +class RenderMosaicGridRow extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { + RenderMosaicGridRow({ + List? children, + required MosaicRowLayout rowLayout, + required double spacing, + required TextDirection textDirection, + }) : _rowLayout = rowLayout, + _spacing = spacing, + _textDirection = textDirection { + addAll(children); + } + + MosaicRowLayout get rowLayout => _rowLayout; + MosaicRowLayout _rowLayout; + + set rowLayout(MosaicRowLayout value) { + if (_rowLayout == value) return; + _rowLayout = value; + markNeedsLayout(); + } + + double get spacing => _spacing; + double _spacing; + + set spacing(double value) { + if (_spacing == value) return; + _spacing = value; + markNeedsLayout(); + } + + TextDirection get textDirection => _textDirection; + TextDirection _textDirection; + + set textDirection(TextDirection value) { + if (_textDirection == value) return; + _textDirection = value; + markNeedsLayout(); + } + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! _GridRowParentData) { + child.parentData = _GridRowParentData(); + } + } + + double get intrinsicWidth => rowLayout.itemWidths.sum + spacing * (childCount - 1); + + @override + double computeMinIntrinsicWidth(double height) => intrinsicWidth; + + @override + double computeMaxIntrinsicWidth(double height) => intrinsicWidth; + + @override + double computeMinIntrinsicHeight(double width) => rowLayout.height; + + @override + double computeMaxIntrinsicHeight(double width) => rowLayout.height; + + @override + void performLayout() { + var child = firstChild; + if (child == null) { + size = constraints.smallest; + return; + } + final thumbnailHeight = rowLayout.height - spacing; + size = Size(constraints.maxWidth, rowLayout.height); + final flipMainAxis = textDirection == TextDirection.rtl; + final sign = (flipMainAxis ? -1.0 : 1.0); + var i = 0; + var offset = Offset(flipMainAxis ? size.width - rowLayout.itemWidths[i] : 0, 0); + while (child != null) { + final thumbnailWidth = rowLayout.itemWidths[i]; + final childConstraints = BoxConstraints.tight(Size(thumbnailWidth, thumbnailHeight)); + child.layout(childConstraints, parentUsesSize: false); + final childParentData = child.parentData! as _GridRowParentData; + childParentData.offset = offset; + final dx = sign * (thumbnailWidth + spacing); + offset += Offset(dx, 0); + child = childParentData.nextSibling; + i++; + } + } + + @override + double? computeDistanceToActualBaseline(TextBaseline baseline) { + return defaultComputeDistanceToHighestActualBaseline(baseline); + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + return defaultHitTestChildren(result, position: position); + } + + @override + void paint(PaintingContext context, Offset offset) { + defaultPaint(context, offset); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('rowLayout', rowLayout)); + properties.add(DoubleProperty('spacing', spacing)); + properties.add(EnumProperty('textDirection', textDirection)); + } +} diff --git a/lib/widgets/common/grid/sections/mosaic/scale_grid.dart b/lib/widgets/common/grid/sections/mosaic/scale_grid.dart new file mode 100644 index 000000000..f199d80ab --- /dev/null +++ b/lib/widgets/common/grid/sections/mosaic/scale_grid.dart @@ -0,0 +1,74 @@ +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/grid/sections/mosaic/scale_overlay.dart'; +import 'package:aves/widgets/common/grid/sections/mosaic/section_layout_builder.dart'; +import 'package:flutter/material.dart'; + +class MosaicGrid extends StatelessWidget { + final Rect contentRect; + final Size tileSize; + final double spacing; + final MosaicItemBuilder builder; + + static const _itemRatios = [ + 3 / 4, + 16 / 9, + 9 / 16, + 3 / 4, + 4 / 3, + 4 / 3, + 3 / 4, + 4 / 3, + 4 / 3, + 4 / 3, + ]; + + const MosaicGrid({ + super.key, + required this.contentRect, + required this.tileSize, + required this.spacing, + required this.builder, + }); + + @override + Widget build(BuildContext context) { + final children = []; + + final targetExtent = tileSize.width; + final rows = MosaicSectionLayoutBuilder.computeMosaicRows( + section: List.generate(5, (i) => _itemRatios).expand((v) => v).toList(), + availableWidthFor: (itemCount) => contentRect.width - (itemCount - 1) * spacing, + heightMax: targetExtent * MosaicSectionLayoutBuilder.heightMaxFactor, + targetExtent: targetExtent, + spacing: spacing, + bottom: tileSize.height - tileSize.width, + coverRatioResolver: (item) => item, + ); + + var i = 0; + var dy = contentRect.top; + rows.forEach((row) { + var dx = contentRect.left; + final itemHeight = row.height - spacing; + row.itemWidths.forEach((itemWidth) { + children.add( + AnimatedPositioned( + left: dx, + top: dy, + width: itemWidth, + height: itemHeight, + duration: Durations.scalingGridPositionAnimation, + child: builder(i, targetExtent), + ), + ); + dx += itemWidth + spacing; + i++; + }); + dy += row.height; + }); + + return Stack( + children: children, + ); + } +} diff --git a/lib/widgets/common/grid/sections/mosaic/scale_overlay.dart b/lib/widgets/common/grid/sections/mosaic/scale_overlay.dart new file mode 100644 index 000000000..07e7ed81d --- /dev/null +++ b/lib/widgets/common/grid/sections/mosaic/scale_overlay.dart @@ -0,0 +1,116 @@ +import 'package:aves/theme/durations.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/grid/sections/mosaic/scale_grid.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:flutter/material.dart'; + +typedef MosaicItemBuilder = Widget Function(int index, double targetExtent); + +class MosaicScaleOverlay extends StatelessWidget { + final Rect contentRect; + final double spacing, extentMax; + final ValueNotifier scaledSizeNotifier; + final MosaicItemBuilder itemBuilder; + + const MosaicScaleOverlay({ + super.key, + required this.contentRect, + required this.spacing, + required this.extentMax, + required this.scaledSizeNotifier, + required this.itemBuilder, + }); + + @override + Widget build(BuildContext context) { + return MediaQueryDataProvider( + child: IgnorePointer( + child: _OverlayBackground( + child: ValueListenableBuilder( + valueListenable: scaledSizeNotifier, + builder: (context, scaledSize, child) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + Widget _buildBar(double width, Color color) => ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(4)), + child: Container( + color: color, + width: width, + height: 4, + ), + ); + return SafeArea( + left: false, + right: false, + bottom: false, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Stack( + alignment: Alignment.center, + children: [ + _buildBar(extentMax, colorScheme.onPrimary.withOpacity(.1)), + _buildBar(scaledSize.width, colorScheme.secondary), + ], + ), + ), + Expanded( + child: MosaicGrid( + contentRect: contentRect, + tileSize: scaledSize, + spacing: spacing, + builder: itemBuilder, + ), + ), + ], + ), + ); + }, + ), + ), + ), + ); + } +} + +class _OverlayBackground extends StatefulWidget { + final Widget child; + + const _OverlayBackground({ + required this.child, + }); + + @override + State<_OverlayBackground> createState() => _OverlayBackgroundState(); +} + +class _OverlayBackgroundState extends State<_OverlayBackground> { + bool _initialized = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => _initialized = true)); + } + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + decoration: _buildBackgroundDecoration(context), + duration: Durations.scalingGridBackgroundAnimation, + child: widget.child, + ); + } + + BoxDecoration _buildBackgroundDecoration(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return _initialized + ? BoxDecoration( + color: isDark ? Colors.black87 : const Color(0xDDFFFFFF), + ) + : BoxDecoration( + color: isDark ? Constants.transparentBlack : Constants.transparentWhite, + ); + } +} diff --git a/lib/widgets/common/grid/sections/mosaic/section_layout.dart b/lib/widgets/common/grid/sections/mosaic/section_layout.dart new file mode 100644 index 000000000..0cba43a3d --- /dev/null +++ b/lib/widgets/common/grid/sections/mosaic/section_layout.dart @@ -0,0 +1,61 @@ +import 'package:aves/widgets/common/grid/sections/section_layout.dart'; +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; + +class MosaicSectionLayout extends SectionLayout { + final List rows; + + @override + List get props => [sectionKey, firstIndex, lastIndex, minOffset, maxOffset, headerExtent, rows, spacing]; + + const MosaicSectionLayout({ + required super.sectionKey, + required super.firstIndex, + required super.lastIndex, + required super.minOffset, + required super.maxOffset, + required super.headerExtent, + required this.rows, + required super.spacing, + required super.builder, + }); + + @override + double indexToLayoutOffset(int index) { + index -= bodyFirstIndex; + if (index < 0) return minOffset; + return bodyMinOffset + (index < rows.length ? rows[index].minOffset : rows.lastOrNull?.maxOffset ?? 0); + } + + @override + int getMinChildIndexForScrollOffset(double scrollOffset) { + scrollOffset -= bodyMinOffset; + if (scrollOffset < 0) return firstIndex; + return bodyFirstIndex + rows.indexWhere((v) => scrollOffset < v.maxOffset); + } + + @override + int getMaxChildIndexForScrollOffset(double scrollOffset) { + scrollOffset -= bodyMinOffset; + if (scrollOffset < 0) return firstIndex; + final rowIndex = rows.indexWhere((v) => scrollOffset < v.maxOffset); + return bodyFirstIndex + (rowIndex == -1 ? rows.length - 1 : rowIndex); + } +} + +class MosaicRowLayout extends Equatable { + final int firstIndex, lastIndex; + final double minOffset, maxOffset, height; + final List itemWidths; + + @override + List get props => [firstIndex, lastIndex, minOffset, maxOffset, height, itemWidths]; + + const MosaicRowLayout({ + required this.firstIndex, + required this.lastIndex, + required this.minOffset, + required this.height, + required this.itemWidths, + }) : maxOffset = minOffset + height; +} diff --git a/lib/widgets/common/grid/sections/mosaic/section_layout_builder.dart b/lib/widgets/common/grid/sections/mosaic/section_layout_builder.dart new file mode 100644 index 000000000..14a7b6bcb --- /dev/null +++ b/lib/widgets/common/grid/sections/mosaic/section_layout_builder.dart @@ -0,0 +1,197 @@ +import 'dart:math'; + +import 'package:aves/model/source/section_keys.dart'; +import 'package:aves/widgets/common/grid/sections/list_layout.dart'; +import 'package:aves/widgets/common/grid/sections/mosaic/list_layout.dart'; +import 'package:aves/widgets/common/grid/sections/mosaic/row.dart'; +import 'package:aves/widgets/common/grid/sections/mosaic/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/provider.dart'; +import 'package:aves/widgets/common/grid/sections/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/section_layout_builder.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:tuple/tuple.dart'; + +class MosaicSectionLayoutBuilder extends SectionLayoutBuilder { + int _currentIndex = 0; + double _currentOffset = 0; + late double Function(int itemCount) rowAvailableWidth; + late double rowHeightMax; + final CoverRatioResolver coverRatioResolver; + + static const heightMaxFactor = 2.4; + + MosaicSectionLayoutBuilder({ + required super.sections, + required super.showHeaders, + required super.getHeaderExtent, + required super.buildHeader, + required super.scrollableWidth, + required super.tileLayout, + required super.columnCount, + required super.spacing, + required super.horizontalPadding, + required super.tileWidth, + required super.tileHeight, + required super.tileBuilder, + required Duration tileAnimationDelay, + required this.coverRatioResolver, + }) : super(tileAnimationDelay: Duration(milliseconds: (tileAnimationDelay.inMilliseconds / columnCount).ceil())) { + final rowWidth = scrollableWidth - horizontalPadding * 2; + rowAvailableWidth = (itemCount) => rowWidth - (itemCount - 1) * spacing; + rowHeightMax = tileWidth * heightMaxFactor; + } + + @override + SectionedListLayout updateLayouts(BuildContext context) { + final sectionLayouts = sections.keys + .map((sectionKey) => buildSectionLayout( + headerExtent: showHeaders ? getHeaderExtent(context, sectionKey) : 0.0, + sectionKey: sectionKey, + section: sections[sectionKey]!, + animate: animate, + )) + .toList(); + + return MosaicSectionedListLayout( + sections: sections, + showHeaders: showHeaders, + spacing: spacing, + horizontalPadding: horizontalPadding, + sectionLayouts: sectionLayouts, + ); + } + + @override + SectionLayout buildSectionLayout({ + required double headerExtent, + required SectionKey sectionKey, + required List section, + required bool animate, + }) { + final rows = computeMosaicRows( + section: section, + availableWidthFor: rowAvailableWidth, + heightMax: rowHeightMax, + targetExtent: tileWidth, + spacing: spacing, + bottom: bottom, + coverRatioResolver: coverRatioResolver, + ); + final rowCount = rows.length; + final sectionChildCount = 1 + rowCount; + + final sectionFirstIndex = _currentIndex; + _currentIndex += sectionChildCount; + final sectionLastIndex = _currentIndex - 1; + + final sectionMinOffset = _currentOffset; + _currentOffset += headerExtent + rows.map((v) => v.height).sum - spacing; + final sectionMaxOffset = _currentOffset; + + return MosaicSectionLayout( + sectionKey: sectionKey, + firstIndex: sectionFirstIndex, + lastIndex: sectionLastIndex, + minOffset: sectionMinOffset, + maxOffset: sectionMaxOffset, + headerExtent: headerExtent, + rows: rows, + spacing: spacing, + builder: (context, listIndex) { + final textDirection = Directionality.of(context); + final sectionChildIndex = listIndex - sectionFirstIndex; + final isHeader = sectionChildIndex == 0; + final row = isHeader ? rows.first : rows[sectionChildIndex - 1]; + final sectionGridIndex = isHeader ? sectionFirstIndex * columnCount : (sectionChildIndex + 1) * columnCount + row.firstIndex; + return buildSectionWidget( + context: context, + section: section, + sectionGridIndex: sectionGridIndex, + sectionChildIndex: sectionChildIndex, + itemIndexRange: () => isHeader ? const Tuple2(0, 0) : Tuple2(row.firstIndex, row.lastIndex + 1), + sectionKey: sectionKey, + headerExtent: headerExtent, + itemSizes: row.itemWidths.map((v) => Size(v, row.height)).toList(), + animate: animate, + buildGridRow: (children) { + return isHeader + ? const SizedBox() + : MosaicGridRow( + rowLayout: row, + spacing: spacing, + textDirection: textDirection, + children: children, + ); + }, + ); + }, + ); + } + + static List computeMosaicRows({ + required List section, + required double Function(int itemCount) availableWidthFor, + required double heightMax, + required double targetExtent, + required double spacing, + required double bottom, + required CoverRatioResolver coverRatioResolver, + }) { + final rows = []; + final items = []; + double ratioSum = 0, ratioMin = double.infinity; + int firstIndex = 0; + double minOffset = 0; + + void addRow(int i, {required bool complete}) { + if (items.isEmpty) return; + + final availableWidth = availableWidthFor(items.length); + var height = availableWidth / ratioSum + spacing; + if (height > heightMax + precisionErrorTolerance) { + if (!complete) { + ratioSum = availableWidth / (heightMax - spacing); + addRow(i, complete: complete); + } + return; + } + + height += bottom; + rows.add(MosaicRowLayout( + firstIndex: firstIndex, + lastIndex: i - 1, + minOffset: minOffset, + height: height, + itemWidths: items.map((item) => availableWidth * coverRatioResolver(item) / ratioSum).toList(), + )); + firstIndex = i; + minOffset += height; + ratioMin = double.infinity; + ratioSum = 0; + items.clear(); + } + + section.forEachIndexed((i, item) { + final ratio = coverRatioResolver(item); + final nextAvailableWidth = availableWidthFor(items.length + 1); + final nextRatioSum = ratio + ratioSum; + final nextItemMinWidth = nextAvailableWidth * min(ratio, ratioMin) / nextRatioSum; + final nextHeight = nextAvailableWidth / nextRatioSum + spacing; + if (nextItemMinWidth < targetExtent || nextHeight < targetExtent) { + // add row when appending the next item would make other items too small + addRow(i, complete: true); + } + items.add(item); + ratioMin = min(ratio, ratioMin); + ratioSum += ratio; + }); + if (items.isNotEmpty) { + // add last row, possibly incomplete + addRow(section.length, complete: false); + } + + return rows; + } +} diff --git a/lib/widgets/common/grid/sections/provider.dart b/lib/widgets/common/grid/sections/provider.dart new file mode 100644 index 000000000..1cc6cf077 --- /dev/null +++ b/lib/widgets/common/grid/sections/provider.dart @@ -0,0 +1,105 @@ +import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves/model/source/section_keys.dart'; +import 'package:aves/widgets/common/grid/sections/fixed/section_layout_builder.dart'; +import 'package:aves/widgets/common/grid/sections/list_layout.dart'; +import 'package:aves/widgets/common/grid/sections/mosaic/section_layout_builder.dart'; +import 'package:aves/widgets/common/grid/sections/section_layout_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:provider/provider.dart'; + +typedef CoverRatioResolver = double Function(T item); + +abstract class SectionedListLayoutProvider extends StatelessWidget { + final double scrollableWidth; + final TileLayout tileLayout; + final int columnCount; + final double spacing, horizontalPadding, tileWidth, tileHeight; + final TileBuilder tileBuilder; + final Duration tileAnimationDelay; + final CoverRatioResolver coverRatioResolver; + final Widget child; + + const SectionedListLayoutProvider({ + super.key, + required this.scrollableWidth, + required this.tileLayout, + required int columnCount, + required this.spacing, + required this.horizontalPadding, + required double tileWidth, + required this.tileHeight, + required this.tileBuilder, + required this.tileAnimationDelay, + required this.coverRatioResolver, + required this.child, + }) : assert(scrollableWidth != 0), + columnCount = tileLayout == TileLayout.list ? 1 : columnCount, + tileWidth = tileLayout == TileLayout.list ? scrollableWidth - (horizontalPadding * 2) : tileWidth; + + @override + Widget build(BuildContext context) { + return ProxyProvider0>( + update: (context, _) { + switch (tileLayout) { + case TileLayout.mosaic: + return MosaicSectionLayoutBuilder( + sections: sections, + showHeaders: showHeaders, + getHeaderExtent: getHeaderExtent, + buildHeader: buildHeader, + scrollableWidth: scrollableWidth, + tileLayout: tileLayout, + columnCount: columnCount, + spacing: spacing, + horizontalPadding: horizontalPadding, + tileWidth: tileWidth, + tileHeight: tileHeight, + tileBuilder: tileBuilder, + tileAnimationDelay: tileAnimationDelay, + coverRatioResolver: coverRatioResolver, + ).updateLayouts(context); + case TileLayout.grid: + case TileLayout.list: + return FixedExtentSectionLayoutBuilder( + sections: sections, + showHeaders: showHeaders, + buildHeader: buildHeader, + getHeaderExtent: getHeaderExtent, + scrollableWidth: scrollableWidth, + tileLayout: tileLayout, + columnCount: columnCount, + spacing: spacing, + horizontalPadding: horizontalPadding, + tileWidth: tileWidth, + tileHeight: tileHeight, + tileBuilder: tileBuilder, + tileAnimationDelay: tileAnimationDelay, + ).updateLayouts(context); + } + }, + child: child, + ); + } + + bool get showHeaders; + + Map> get sections; + + double getHeaderExtent(BuildContext context, SectionKey sectionKey); + + Widget buildHeader(BuildContext context, SectionKey sectionKey, double headerExtent); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('scrollableWidth', scrollableWidth)); + properties.add(EnumProperty('tileLayout', tileLayout)); + properties.add(IntProperty('columnCount', columnCount)); + properties.add(DoubleProperty('spacing', spacing)); + properties.add(DoubleProperty('horizontalPadding', horizontalPadding)); + properties.add(DoubleProperty('tileWidth', tileWidth)); + properties.add(DoubleProperty('tileHeight', tileHeight)); + properties.add(DiagnosticsProperty('showHeaders', showHeaders)); + } +} diff --git a/lib/widgets/common/grid/sections/section_layout.dart b/lib/widgets/common/grid/sections/section_layout.dart new file mode 100644 index 000000000..ccc7b993b --- /dev/null +++ b/lib/widgets/common/grid/sections/section_layout.dart @@ -0,0 +1,37 @@ +import 'package:aves/model/source/section_keys.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +@immutable +abstract class SectionLayout extends Equatable { + final SectionKey sectionKey; + final int firstIndex, lastIndex, bodyFirstIndex; + final double minOffset, maxOffset, bodyMinOffset; + final double headerExtent, spacing; + final IndexedWidgetBuilder builder; + + @override + List get props => [sectionKey, firstIndex, lastIndex, minOffset, maxOffset, headerExtent, spacing]; + + const SectionLayout({ + required this.sectionKey, + required this.firstIndex, + required this.lastIndex, + required this.minOffset, + required this.maxOffset, + required this.headerExtent, + required this.spacing, + required this.builder, + }) : bodyFirstIndex = firstIndex + 1, + bodyMinOffset = minOffset + headerExtent; + + bool hasChild(int index) => firstIndex <= index && index <= lastIndex; + + bool hasChildAtOffset(double scrollOffset) => minOffset <= scrollOffset && scrollOffset <= maxOffset; + + double indexToLayoutOffset(int index); + + int getMinChildIndexForScrollOffset(double scrollOffset); + + int getMaxChildIndexForScrollOffset(double scrollOffset); +} diff --git a/lib/widgets/common/grid/sections/section_layout_builder.dart b/lib/widgets/common/grid/sections/section_layout_builder.dart new file mode 100644 index 000000000..6b7570d8f --- /dev/null +++ b/lib/widgets/common/grid/sections/section_layout_builder.dart @@ -0,0 +1,106 @@ +import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves/model/source/section_keys.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/grid/sections/list_layout.dart'; +import 'package:aves/widgets/common/grid/sections/section_layout.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; + +typedef TileBuilder = Widget Function(T item, Size tileSize); + +abstract class SectionLayoutBuilder { + final Map> sections; + final bool showHeaders; + final double Function(BuildContext context, SectionKey sectionKey) getHeaderExtent; + final Widget Function(BuildContext context, SectionKey sectionKey, double headerExtent) buildHeader; + final double scrollableWidth; + final TileLayout tileLayout; + final int columnCount; + final double spacing, horizontalPadding, tileWidth, tileHeight, bottom; + final TileBuilder tileBuilder; + final Duration tileAnimationDelay; + final bool animate; + + const SectionLayoutBuilder({ + required this.sections, + required this.showHeaders, + required this.getHeaderExtent, + required this.buildHeader, + required this.scrollableWidth, + required this.tileLayout, + required this.columnCount, + required this.spacing, + required this.horizontalPadding, + required this.tileWidth, + required this.tileHeight, + required this.tileBuilder, + required this.tileAnimationDelay, + }) : animate = tileAnimationDelay > Duration.zero, + bottom = tileHeight - tileWidth; + + SectionedListLayout updateLayouts(BuildContext context); + + SectionLayout buildSectionLayout({ + required double headerExtent, + required SectionKey sectionKey, + required List section, + required bool animate, + }); + + Widget buildSectionWidget({ + required BuildContext context, + required List section, + required int sectionGridIndex, + required int sectionChildIndex, + required Tuple2 Function() itemIndexRange, + required SectionKey sectionKey, + required double headerExtent, + required List itemSizes, + required bool animate, + required Widget Function(List children) buildGridRow, + }) { + if (sectionChildIndex == 0) { + final header = headerExtent > 0 ? buildHeader(context, sectionKey, headerExtent) : const SizedBox(); + return animate ? _buildAnimation(context, sectionGridIndex, header) : header; + } + + final sectionItemCount = section.length; + final itemMinMax = itemIndexRange(); + final minItemIndex = itemMinMax.item1.clamp(0, sectionItemCount); + final maxItemIndex = itemMinMax.item2.clamp(0, sectionItemCount); + final childrenCount = maxItemIndex - minItemIndex; + final children = []; + for (var i = 0; i < childrenCount; i++) { + final item = RepaintBoundary( + child: tileBuilder(section[minItemIndex + i], itemSizes[i]), + ); + if (animate) { + children.add(_buildAnimation(context, sectionGridIndex + i, item)); + } else { + children.add(item); + } + } + return Padding( + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + child: buildGridRow(children), + ); + } + + Widget _buildAnimation(BuildContext context, int index, Widget child) { + final durations = context.watch(); + return AnimationConfiguration.staggeredGrid( + position: index, + columnCount: tileLayout == TileLayout.mosaic ? 1 : columnCount, + duration: durations.staggeredAnimation, + delay: tileAnimationDelay, + child: SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: child, + ), + ), + ); + } +} diff --git a/lib/widgets/common/grid/selector.dart b/lib/widgets/common/grid/selector.dart index 4a53f5607..20c0c98f7 100644 --- a/lib/widgets/common/grid/selector.dart +++ b/lib/widgets/common/grid/selector.dart @@ -5,7 +5,7 @@ import 'package:aves/model/selection.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; -import 'package:aves/widgets/common/grid/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/list_layout.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/common/grid/sliver.dart b/lib/widgets/common/grid/sliver.dart index dffeeb407..5be6b8415 100644 --- a/lib/widgets/common/grid/sliver.dart +++ b/lib/widgets/common/grid/sliver.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; -import 'package:aves/widgets/common/grid/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/list_layout.dart'; +import 'package:aves/widgets/common/grid/sections/section_layout.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -10,7 +11,7 @@ import 'package:provider/provider.dart'; // Use a `SliverList` instead of multiple `SliverGrid` because having one `SliverGrid` per section does not scale up. // With the multiple `SliverGrid` solution, thumbnails at the beginning of each sections are built even though they are offscreen -// because of `RenderSliverMultiBoxAdaptor.addInitialChild` called by `RenderSliverGrid.performLayout` (line 547), as of Flutter v1.17.0. +// because of `RenderSliverMultiBoxAdaptor.addInitialChild` called by `RenderSliverGrid.performLayout` (line 591), as of Flutter v3.3.3. // cf https://github.com/flutter/flutter/issues/49027 // adapted from Flutter `RenderSliverFixedExtentBoxAdaptor` in `/rendering/sliver_fixed_extent_list.dart` class SectionedListSliver extends StatelessWidget { @@ -26,7 +27,7 @@ class SectionedListSliver extends StatelessWidget { (context, index) { if (index >= childCount) return null; final sectionLayout = sectionLayouts.firstWhereOrNull((section) => section.hasChild(index)); - return sectionLayout?.builder(context, index) ?? const SizedBox.shrink(); + return sectionLayout?.builder(context, index) ?? const SizedBox(); }, childCount: childCount, addAutomaticKeepAlives: false, diff --git a/lib/widgets/common/identity/aves_caption.dart b/lib/widgets/common/identity/aves_caption.dart new file mode 100644 index 000000000..470357ab7 --- /dev/null +++ b/lib/widgets/common/identity/aves_caption.dart @@ -0,0 +1,42 @@ +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/basic/animated_text.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AvesCaption extends StatelessWidget { + final String data; + + const AvesCaption( + this.data, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final subtitleStyle = theme.textTheme.bodySmall!; + final subtitleChangeShadowColor = theme.colorScheme.onPrimary; + return AnimatedText( + // provide key to refresh on theme brightness change + key: ValueKey(subtitleChangeShadowColor), + data, + style: subtitleStyle.copyWith( + shadows: [ + Shadow( + color: subtitleChangeShadowColor.withOpacity(0), + blurRadius: 0, + ) + ], + ), + changedStyle: subtitleStyle.copyWith( + shadows: [ + Shadow( + color: subtitleChangeShadowColor, + blurRadius: 3, + ) + ], + ), + duration: context.read().formTextStyleTransition, + ); + } +} diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 6d58cb513..7f30ff28a 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -15,6 +15,7 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/filter_bar.dart'; import 'package:aves/widgets/common/basic/menu.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:flutter/material.dart'; @@ -96,6 +97,7 @@ class AvesFilterChip extends StatefulWidget { if (filter is AlbumFilter) ChipAction.goToAlbumPage, if ((filter is LocationFilter && filter.level == LocationLevel.country)) ChipAction.goToCountryPage, if (filter is TagFilter) ChipAction.goToTagPage, + ChipAction.reverse, ChipAction.hide, ]; @@ -113,12 +115,20 @@ class AvesFilterChip extends StatefulWidget { child: Text(filter.getLabel(context)), ), const PopupMenuDivider(), - ...actions.map((action) => PopupMenuItem( - value: action, - child: MenuIconTheme( - child: MenuRow(text: action.getText(context), icon: action.getIcon()), - ), - )), + ...actions.map((action) { + late String text; + if (action == ChipAction.reverse) { + text = filter.reversed ? context.l10n.chipActionFilterIn : context.l10n.chipActionFilterOut; + } else { + text = action.getText(context); + } + return PopupMenuItem( + value: action, + child: MenuIconTheme( + child: MenuRow(text: text, icon: action.getIcon()), + ), + ); + }), ], ); if (selectedAction != null) { @@ -229,7 +239,8 @@ class _AvesFilterChipState extends State { filter.getLabel(context), style: TextStyle( fontSize: AvesFilterChip.fontSize, - decoration: filter.not ? TextDecoration.lineThrough : null, + decoration: filter.reversed ? TextDecoration.lineThrough : null, + decorationThickness: 2, ), softWrap: false, overflow: TextOverflow.fade, diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index 700f16469..56f89de0f 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -281,10 +281,15 @@ class OverlayIcon extends StatelessWidget { children: [ iconChild, const SizedBox(width: 2), - Text( - text!, - // consistent with the color used for the icon next to it - style: TextStyle(color: IconTheme.of(context).color), + Flexible( + child: Text( + text!, + // consistent with the color used for the icon next to it + style: TextStyle(color: IconTheme.of(context).color), + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), ), ], ), diff --git a/lib/widgets/common/magnifier/scale/scale_boundaries.dart b/lib/widgets/common/magnifier/scale/scale_boundaries.dart index e2190afac..bba15200e 100644 --- a/lib/widgets/common/magnifier/scale/scale_boundaries.dart +++ b/lib/widgets/common/magnifier/scale/scale_boundaries.dart @@ -45,7 +45,7 @@ class ScaleBoundaries extends Equatable { ); } - double _scaleForLevel(ScaleLevel level) { + double scaleForLevel(ScaleLevel level) { final factor = level.factor; switch (level.ref) { case ScaleReference.contained: @@ -61,18 +61,18 @@ class ScaleBoundaries extends Equatable { double get originalScale => 1.0 / window.devicePixelRatio; double get minScale => { - _scaleForLevel(_minScale), + scaleForLevel(_minScale), _allowOriginalScaleBeyondRange ? originalScale : double.infinity, initialScale, }.fold(double.infinity, min); double get maxScale => { - _scaleForLevel(_maxScale), + scaleForLevel(_maxScale), _allowOriginalScaleBeyondRange ? originalScale : double.negativeInfinity, initialScale, }.fold(0, max); - double get initialScale => _scaleForLevel(_initialScale); + double get initialScale => scaleForLevel(_initialScale); Offset get _viewportCenter => viewportSize.center(Offset.zero); diff --git a/lib/widgets/common/map/attribution.dart b/lib/widgets/common/map/attribution.dart index 57aaab097..2b50b7199 100644 --- a/lib/widgets/common/map/attribution.dart +++ b/lib/widgets/common/map/attribution.dart @@ -35,7 +35,7 @@ class Attribution extends StatelessWidget { selectable: true, styleSheet: MarkdownStyleSheet( a: TextStyle(color: theme.colorScheme.secondary), - p: theme.textTheme.caption!.merge(const TextStyle(fontSize: InfoRowGroup.fontSize)), + p: theme.textTheme.bodySmall!.merge(const TextStyle(fontSize: InfoRowGroup.fontSize)), ), onTapLink: (text, href, title) async { if (href != null) { diff --git a/lib/widgets/common/providers/tile_extent_controller_provider.dart b/lib/widgets/common/providers/tile_extent_controller_provider.dart index c2e53f309..0a5d45b99 100644 --- a/lib/widgets/common/providers/tile_extent_controller_provider.dart +++ b/lib/widgets/common/providers/tile_extent_controller_provider.dart @@ -15,14 +15,10 @@ class TileExtentControllerProvider extends StatelessWidget { @override Widget build(BuildContext context) { return LayoutBuilder( - builder: (context, constraints) { - return LayoutBuilder( - builder: (context, constraints) => ProxyProvider0( - update: (context, __) => controller..setViewportSize(constraints.biggest), - child: child, - ), - ); - }, + builder: (context, constraints) => ProxyProvider0( + update: (context, __) => controller..setViewportSize(constraints.biggest), + child: child, + ), ); } } diff --git a/lib/widgets/common/search/delegate.dart b/lib/widgets/common/search/delegate.dart index 066efcb3b..092bb25ad 100644 --- a/lib/widgets/common/search/delegate.dart +++ b/lib/widgets/common/search/delegate.dart @@ -18,7 +18,7 @@ abstract class AvesSearchDelegate extends SearchDelegate { } @override - Widget buildLeading(BuildContext context) { + Widget? buildLeading(BuildContext context) { // use a property instead of checking `Navigator.canPop(context)` // because the navigator state changes as soon as we press back // so the leading may mistakenly switch to the close button @@ -37,7 +37,7 @@ abstract class AvesSearchDelegate extends SearchDelegate { } @override - List buildActions(BuildContext context) { + List? buildActions(BuildContext context) { return [ if (query.isNotEmpty) IconButton( @@ -91,6 +91,9 @@ abstract class AvesSearchDelegate extends SearchDelegate { @override set query(String value) { queryTextController.text = value; + if (queryTextController.text.isNotEmpty) { + queryTextController.selection = TextSelection.fromPosition(TextPosition(offset: queryTextController.text.length)); + } } final ValueNotifier currentBodyNotifier = ValueNotifier(null); diff --git a/lib/widgets/common/search/page.dart b/lib/widgets/common/search/page.dart index 50cb90bdb..15ac0ccdd 100644 --- a/lib/widgets/common/search/page.dart +++ b/lib/widgets/common/search/page.dart @@ -38,7 +38,7 @@ class _SearchPageState extends State { @override void didUpdateWidget(covariant SearchPage oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.delegate != oldWidget.delegate) { + if (oldWidget.delegate != widget.delegate) { _unregisterWidget(oldWidget); _registerWidget(widget); } @@ -138,7 +138,7 @@ class _SearchPageState extends State { hintStyle: theme.inputDecorationTheme.hintStyle, ), textInputAction: TextInputAction.search, - style: theme.textTheme.headline6, + style: theme.textTheme.titleLarge, onSubmitted: (_) => widget.delegate.showResults(context), ), ), diff --git a/lib/widgets/common/search/route.dart b/lib/widgets/common/search/route.dart index 59448ace5..93bf133f0 100644 --- a/lib/widgets/common/search/route.dart +++ b/lib/widgets/common/search/route.dart @@ -14,7 +14,7 @@ class SearchPageRoute extends PageRoute { delegate.route == null, 'The ${delegate.runtimeType} instance is currently used by another active ' 'search. Please close that search by calling close() on the SearchDelegate ' - 'before openening another search with the same delegate instance.', + 'before opening another search with the same delegate instance.', ); delegate.route = this; } diff --git a/lib/widgets/common/thumbnail/decorated.dart b/lib/widgets/common/thumbnail/decorated.dart index 336a3f8c9..9871d3502 100644 --- a/lib/widgets/common/thumbnail/decorated.dart +++ b/lib/widgets/common/thumbnail/decorated.dart @@ -9,7 +9,7 @@ class DecoratedThumbnail extends StatelessWidget { final AvesEntry entry; final double tileExtent; final ValueNotifier? cancellableNotifier; - final bool selectable, highlightable; + final bool isMosaic, selectable, highlightable; final Object? Function()? heroTagger; static final Color borderColor = Colors.grey.shade700; @@ -20,6 +20,7 @@ class DecoratedThumbnail extends StatelessWidget { required this.entry, required this.tileExtent, this.cancellableNotifier, + this.isMosaic = false, this.selectable = true, this.highlightable = true, this.heroTagger, @@ -27,9 +28,13 @@ class DecoratedThumbnail extends StatelessWidget { @override Widget build(BuildContext context) { + final thumbnailWidth = isMosaic ? tileExtent * entry.displayAspectRatio : tileExtent; + final thumbnailHeight = tileExtent; + Widget child = ThumbnailImage( entry: entry, extent: tileExtent, + isMosaic: isMosaic, cancellableNotifier: cancellableNotifier, heroTag: heroTagger?.call(), ); @@ -57,8 +62,8 @@ class DecoratedThumbnail extends StatelessWidget { width: borderWidth, )), ), - width: tileExtent, - height: tileExtent, + width: thumbnailWidth, + height: thumbnailHeight, child: child, ); } diff --git a/lib/widgets/common/thumbnail/image.dart b/lib/widgets/common/thumbnail/image.dart index 09d29853f..8567539e6 100644 --- a/lib/widgets/common/thumbnail/image.dart +++ b/lib/widgets/common/thumbnail/image.dart @@ -19,7 +19,7 @@ import 'package:provider/provider.dart'; class ThumbnailImage extends StatefulWidget { final AvesEntry entry; final double extent; - final bool progressive; + final bool isMosaic, progressive; final BoxFit? fit; final bool showLoadingBackground; final ValueNotifier? cancellableNotifier; @@ -30,6 +30,7 @@ class ThumbnailImage extends StatefulWidget { required this.entry, required this.extent, this.progressive = true, + this.isMosaic = false, this.fit, this.showLoadingBackground = true, this.cancellableNotifier, @@ -38,6 +39,14 @@ class ThumbnailImage extends StatefulWidget { @override State createState() => _ThumbnailImageState(); + + static Color computeLoadingBackgroundColor(int hashCode, Brightness brightness) { + var rgb = 0x30 + hashCode % 0x20; + if (brightness == Brightness.light) { + rgb = 0xFF - rgb; + } + return Color.fromARGB(0xFF, rgb, rgb, rgb); + } } class _ThumbnailImageState extends State { @@ -52,6 +61,8 @@ class _ThumbnailImageState extends State { double get extent => widget.extent; + bool get isMosaic => widget.isMosaic; + @override void initState() { super.initState(); @@ -180,13 +191,7 @@ class _ThumbnailImageState extends State { Color? _loadingBackgroundColor; Color loadingBackgroundColor(BuildContext context) { - if (_loadingBackgroundColor == null) { - var rgb = 0x30 + entry.uri.hashCode % 0x20; - if (Theme.of(context).brightness == Brightness.light) { - rgb = 0xFF - rgb; - } - _loadingBackgroundColor = Color.fromARGB(0xFF, rgb, rgb, rgb); - } + _loadingBackgroundColor ??= ThumbnailImage.computeLoadingBackgroundColor(entry.uri.hashCode, Theme.of(context).brightness); return _loadingBackgroundColor!; } @@ -200,20 +205,30 @@ class _ThumbnailImageState extends State { // use `RawImage` instead of `Image`, using `ImageInfo` to check dimensions // and have more control when chaining image providers - final fit = widget.fit ?? (entry.isSvg ? BoxFit.contain : BoxFit.cover); + final thumbnailWidth = isMosaic ? extent * entry.displayAspectRatio : extent; + final thumbnailHeight = extent; + final canHaveAlpha = entry.canHaveAlpha; + + final fit = widget.fit ?? + (entry.isSvg + ? BoxFit.contain + : isMosaic + ? BoxFit.contain + : BoxFit.cover); final imageInfo = _lastImageInfo; Widget image = imageInfo == null ? Container( color: widget.showLoadingBackground ? loadingBackgroundColor(context) : Colors.transparent, - width: extent, - height: extent, + width: thumbnailWidth, + height: thumbnailHeight, ) : Selector( selector: (context, s) => s.imageBackground, builder: (context, background, child) { - final backgroundColor = background.isColor ? background.color : null; + // avoid background color filter or layer when the entry cannot be transparent + final backgroundColor = canHaveAlpha && background.isColor ? background.color : null; - if (background == EntryBackground.checkered) { + if (canHaveAlpha && background == EntryBackground.checkered) { return LayoutBuilder( builder: (context, constraints) { final availableSize = constraints.biggest; @@ -240,8 +255,8 @@ class _ThumbnailImageState extends State { return RawImage( image: imageInfo.image, debugImageLabel: imageInfo.debugLabel, - width: extent, - height: extent, + width: thumbnailWidth, + height: thumbnailHeight, scale: imageInfo.scale, color: backgroundColor, colorBlendMode: BlendMode.dstOver, @@ -259,6 +274,8 @@ class _ThumbnailImageState extends State { return TransitionImage( image: entry.bestCachedThumbnail, animation: animation, + thumbnailFit: isMosaic ? BoxFit.contain : BoxFit.cover, + viewerFit: BoxFit.contain, background: backgroundColor, ); }, diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 12751b2d3..ed0f20673 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -173,7 +173,7 @@ class _AppDebugPageState extends State { final source = context.read(); settings.changeFilterVisibility(settings.hiddenFilters, true); settings.changeFilterVisibility({ - TagFilter('aves-thumbnail', not: true), + TagFilter('aves-thumbnail', reversed: true), }, false); await favourites.clear(); await favourites.add(source.visibleEntries); diff --git a/lib/widgets/dialogs/aves_selection_dialog.dart b/lib/widgets/dialogs/aves_selection_dialog.dart index a06c385f6..473fd7824 100644 --- a/lib/widgets/dialogs/aves_selection_dialog.dart +++ b/lib/widgets/dialogs/aves_selection_dialog.dart @@ -56,18 +56,34 @@ class _AvesSelectionDialogState extends State> { @override Widget build(BuildContext context) { + final title = widget.title; final message = widget.message; final confirmationButtonLabel = widget.confirmationButtonLabel; final needConfirmation = confirmationButtonLabel != null; return AvesDialog( - title: widget.title, + title: title, scrollableContent: [ + if (title == null && message == null) SizedBox(height: AvesDialog.cornerRadius.y), if (message != null) Padding( padding: const EdgeInsets.all(16), child: Text(message), ), - ...widget.options.entries.map((kv) => _buildRadioListTile(kv.key, kv.value, needConfirmation)), + ...widget.options.entries.map((kv) { + final radioValue = kv.key; + final radioTitle = kv.value; + return SelectionRadioListTile( + // key is expected by test driver + key: Key(radioValue.toString()), + value: radioValue, + title: radioTitle, + optionSubtitleBuilder: widget.optionSubtitleBuilder, + needConfirmation: needConfirmation, + dense: widget.dense, + getGroupValue: () => _selectedValue, + setGroupValue: (v) => setState(() => _selectedValue = v), + ); + }), ], actions: [ TextButton( @@ -82,17 +98,39 @@ class _AvesSelectionDialogState extends State> { ], ); } +} - Widget _buildRadioListTile(T value, String title, bool needConfirmation) { - final subtitle = widget.optionSubtitleBuilder?.call(value); +class SelectionRadioListTile extends StatelessWidget { + final T value; + final String title; + final TextBuilder? optionSubtitleBuilder; + final bool needConfirmation; + final bool? dense; + final T Function() getGroupValue; + final void Function(T value) setGroupValue; + + const SelectionRadioListTile({ + super.key, + required this.value, + required this.title, + this.optionSubtitleBuilder, + required this.needConfirmation, + this.dense, + required this.getGroupValue, + required this.setGroupValue, + }); + + @override + Widget build(BuildContext context) { + final subtitle = optionSubtitleBuilder?.call(value); return ReselectableRadioListTile( // key is expected by test driver key: Key(value.toString()), value: value, - groupValue: _selectedValue, + groupValue: getGroupValue(), onChanged: (v) { if (needConfirmation) { - setState(() => _selectedValue = v as T); + setGroupValue(v as T); } else { Navigator.pop(context, v); } @@ -112,7 +150,7 @@ class _AvesSelectionDialogState extends State> { maxLines: 1, ) : null, - dense: widget.dense, + dense: dense, ); } } diff --git a/lib/widgets/dialogs/entry_editors/rename_entry_set_dialog.dart b/lib/widgets/dialogs/entry_editors/rename_entry_set_dialog.dart index 6b718583b..01a99a31c 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_set_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_set_dialog.dart @@ -148,7 +148,7 @@ class _RenameEntrySetPageState extends State { children: [ Text( sourceName, - style: TextStyle(color: Theme.of(context).textTheme.caption!.color), + style: TextStyle(color: Theme.of(context).textTheme.bodySmall!.color), softWrap: false, overflow: TextOverflow.fade, maxLines: 1, @@ -172,7 +172,7 @@ class _RenameEntrySetPageState extends State { ); }, separatorBuilder: (context, index) => const SizedBox( - height: CollectionGrid.spacing, + height: CollectionGrid.fixedExtentLayoutSpacing, ), itemCount: min(entryCount, previewMax), ), diff --git a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart index fd4e72cc3..281aac356 100644 --- a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart @@ -449,7 +449,7 @@ class _CoverSelectionDialogState extends State { }.fold('', (previousValue, element) => '$previousValue\n$element'); final para = RenderParagraph( - TextSpan(text: _optionLines, style: Theme.of(context).textTheme.subtitle1!), + TextSpan(text: _optionLines, style: Theme.of(context).textTheme.titleMedium!), textDirection: TextDirection.ltr, textScaleFactor: MediaQuery.textScaleFactorOf(context), )..layout(const BoxConstraints(), parentUsesSize: true); diff --git a/lib/widgets/dialogs/location_pick_dialog.dart b/lib/widgets/dialogs/location_pick_dialog.dart index 6879d8603..480f3d060 100644 --- a/lib/widgets/dialogs/location_pick_dialog.dart +++ b/lib/widgets/dialogs/location_pick_dialog.dart @@ -265,7 +265,7 @@ class _AddressRowState extends State<_AddressRow> { // addresses can include non-latin scripts with inconsistent line height, // which is especially an issue for relayout/painting of heavy Google map, // so we give extra height to give breathing room to the text and stabilize layout - height: Theme.of(context).textTheme.bodyText2!.fontSize! * context.select((mq) => mq.textScaleFactor) * 2, + height: Theme.of(context).textTheme.bodyMedium!.fontSize! * context.select((mq) => mq.textScaleFactor) * 2, child: ValueListenableBuilder( valueListenable: _addressLineNotifier, builder: (context, addressLine, child) { diff --git a/lib/widgets/dialogs/tile_view_dialog.dart b/lib/widgets/dialogs/tile_view_dialog.dart index a4074732e..e00c57d50 100644 --- a/lib/widgets/dialogs/tile_view_dialog.dart +++ b/lib/widgets/dialogs/tile_view_dialog.dart @@ -3,6 +3,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/basic/text_dropdown_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_caption.dart'; import 'package:aves/widgets/common/identity/highlight_title.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -12,18 +13,18 @@ import 'aves_dialog.dart'; class TileViewDialog extends StatefulWidget { final Tuple4 initialValue; - final Map sortOptions; - final Map groupOptions; - final Map layoutOptions; + final List> sortOptions; + final List> groupOptions; + final List> layoutOptions; final String Function(S sort, bool reverse) sortOrder; final bool Function(S? sort, G? group, L? layout)? canGroup; const TileViewDialog({ super.key, required this.initialValue, - this.sortOptions = const {}, - this.groupOptions = const {}, - this.layoutOptions = const {}, + this.sortOptions = const [], + this.groupOptions = const [], + this.layoutOptions = const [], required this.sortOrder, this.canGroup, }); @@ -38,11 +39,11 @@ class _TileViewDialogState extends State> with late L? _selectedLayout; late bool _reverseSort; - Map get sortOptions => widget.sortOptions; + List> get sortOptions => widget.sortOptions; - Map get groupOptions => widget.groupOptions; + List> get groupOptions => widget.groupOptions; - Map get layoutOptions => widget.layoutOptions; + List> get layoutOptions => widget.layoutOptions; bool get canGroup => (widget.canGroup ?? (s, g, l) => true).call(_selectedSort, _selectedGroup, _selectedLayout); @@ -76,12 +77,7 @@ class _TileViewDialogState extends State> with _selectedSort = v; _reverseSort = false; }, - bottom: _selectedSort != null - ? Text( - widget.sortOrder(_selectedSort as S, _reverseSort), - style: Theme.of(context).textTheme.caption, - ) - : null, + bottom: _selectedSort != null ? AvesCaption(widget.sortOrder(_selectedSort as S, _reverseSort)) : null, ), AnimatedSwitcher( duration: context.read().formTransition, @@ -131,7 +127,7 @@ class _TileViewDialogState extends State> with required IconData icon, required String title, Widget? trailing, - required Map options, + required List> options, required T value, required ValueChanged onChanged, Widget? bottom, @@ -171,8 +167,9 @@ class _TileViewDialogState extends State> with Padding( padding: EdgeInsetsDirectional.only(start: iconSize + 16, end: 12), child: TextDropdownButton( - values: options.keys.toList(), - valueText: (v) => options[v] ?? v.toString(), + 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, @@ -190,3 +187,16 @@ class _TileViewDialogState extends State> with ); } } + +@immutable +class TileViewDialogOption { + final T value; + final String title; + final IconData icon; + + const TileViewDialogOption({ + required this.value, + required this.title, + required this.icon, + }); +} diff --git a/lib/widgets/dialogs/wallpaper_settings_dialog.dart b/lib/widgets/dialogs/wallpaper_settings_dialog.dart new file mode 100644 index 000000000..ba8d6ce73 --- /dev/null +++ b/lib/widgets/dialogs/wallpaper_settings_dialog.dart @@ -0,0 +1,53 @@ +import 'package:aves/model/device.dart'; +import 'package:aves/model/wallpaper_target.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:tuple/tuple.dart'; + +import 'aves_dialog.dart'; + +class WallpaperSettingsDialog extends StatefulWidget { + const WallpaperSettingsDialog({super.key}); + + @override + State createState() => _WallpaperSettingsDialogState(); +} + +class _WallpaperSettingsDialogState extends State { + WallpaperTarget _selectedTarget = WallpaperTarget.home; + bool _useScrollEffect = true; + + @override + Widget build(BuildContext context) { + return AvesDialog( + scrollableContent: [ + if (device.canSetLockScreenWallpaper) + ...WallpaperTarget.values.map((value) { + return SelectionRadioListTile( + value: value, + title: value.getName(context), + needConfirmation: true, + getGroupValue: () => _selectedTarget, + setGroupValue: (v) => setState(() => _selectedTarget = v), + ); + }), + SwitchListTile( + value: _useScrollEffect, + onChanged: (v) => setState(() => _useScrollEffect = v), + title: Text(context.l10n.wallpaperUseScrollEffect), + ) + ], + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + ), + TextButton( + onPressed: () => Navigator.pop(context, 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 39160aee4..eade6bd8f 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -118,6 +118,17 @@ class AlbumListPage extends StatelessWidget { if (sections.containsKey(regularKey)) regularKey: sections[regularKey]!, }; break; + case AlbumChipGroupFactor.mimeType: + final visibleEntries = source.visibleEntries; + sections = groupBy, ChipSectionKey>(unpinnedMapEntries, (kv) { + final matches = visibleEntries.where(kv.filter.test); + final hasImage = matches.any((v) => v.isImage); + final hasVideo = matches.any((v) => v.isVideo); + if (hasImage && !hasVideo) return MimeTypeSectionKey.images(context); + if (!hasImage && hasVideo) return MimeTypeSectionKey.videos(context); + return MimeTypeSectionKey.mixed(context); + }); + break; case AlbumChipGroupFactor.volume: sections = groupBy, ChipSectionKey>(unpinnedMapEntries, (kv) { return StorageVolumeSectionKey(context, androidFileUtils.getStorageVolume(kv.filter.album)); 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 c2945a12e..7854c88f3 100644 --- a/lib/widgets/filter_grids/common/action_delegates/album_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/album_set.dart @@ -59,6 +59,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with static const _groupOptions = [ AlbumChipGroupFactor.importance, + AlbumChipGroupFactor.mimeType, AlbumChipGroupFactor.volume, AlbumChipGroupFactor.none, ]; @@ -149,9 +150,9 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with builder: (context) { return TileViewDialog( initialValue: initialValue, - sortOptions: Map.fromEntries(ChipSetActionDelegate.sortOptions.map((v) => MapEntry(v, v.getName(context)))), - groupOptions: Map.fromEntries(_groupOptions.map((v) => MapEntry(v, v.getName(context)))), - layoutOptions: Map.fromEntries(ChipSetActionDelegate.layoutOptions.map((v) => MapEntry(v, v.getName(context)))), + sortOptions: ChipSetActionDelegate.sortOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(), + groupOptions: _groupOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(), + layoutOptions: ChipSetActionDelegate.layoutOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(), sortOrder: (factor, reverse) => factor.getOrderName(context, reverse), ); }, diff --git a/lib/widgets/filter_grids/common/action_delegates/chip.dart b/lib/widgets/filter_grids/common/action_delegates/chip.dart index 3e86c12db..1d75b73cd 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip.dart @@ -13,9 +13,6 @@ import 'package:provider/provider.dart'; class ChipActionDelegate { void onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) { switch (action) { - case ChipAction.hide: - _hide(context, filter); - break; case ChipAction.goToAlbumPage: _goTo(context, filter, AlbumListPage.routeName, (context) => const AlbumListPage()); break; @@ -25,11 +22,34 @@ class ChipActionDelegate { case ChipAction.goToTagPage: _goTo(context, filter, TagListPage.routeName, (context) => const TagListPage()); break; + case ChipAction.reverse: + ReverseFilterNotification(filter).dispatch(context); + break; + case ChipAction.hide: + _hide(context, filter); + break; default: break; } } + void _goTo( + BuildContext context, + CollectionFilter filter, + String routeName, + WidgetBuilder pageBuilder, + ) { + context.read().set(filter); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + settings: RouteSettings(name: routeName), + builder: pageBuilder, + ), + (route) => false, + ); + } + Future _hide(BuildContext context, CollectionFilter filter) async { final confirmed = await showDialog( context: context, @@ -53,21 +73,11 @@ class ChipActionDelegate { settings.changeFilterVisibility({filter}, false); } - - void _goTo( - BuildContext context, - CollectionFilter filter, - String routeName, - WidgetBuilder pageBuilder, - ) { - context.read().set(filter); - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - settings: RouteSettings(name: routeName), - builder: pageBuilder, - ), - (route) => false, - ); - } +} + +@immutable +class ReverseFilterNotification extends Notification { + final CollectionFilter reversedFilter; + + ReverseFilterNotification(CollectionFilter filter) : reversedFilter = filter.reverse(); } 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 5455c9a7b..b697b8fa6 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -54,6 +54,7 @@ abstract class ChipSetActionDelegate with FeedbackMi ]; static const layoutOptions = [ + TileLayout.mosaic, TileLayout.grid, TileLayout.list, ]; @@ -222,8 +223,8 @@ abstract class ChipSetActionDelegate with FeedbackMi builder: (context) { return TileViewDialog( initialValue: initialValue, - sortOptions: Map.fromEntries(sortOptions.map((v) => MapEntry(v, v.getName(context)))), - layoutOptions: Map.fromEntries(layoutOptions.map((v) => MapEntry(v, v.getName(context)))), + sortOptions: sortOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(), + layoutOptions: layoutOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(), sortOrder: (factor, reverse) => factor.getOrderName(context, reverse), ); }, diff --git a/lib/widgets/filter_grids/common/covered_filter_chip.dart b/lib/widgets/filter_grids/common/covered_filter_chip.dart index 97b576fff..6436cb98a 100644 --- a/lib/widgets/filter_grids/common/covered_filter_chip.dart +++ b/lib/widgets/filter_grids/common/covered_filter_chip.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'package:aves/model/covers.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/filters/location.dart'; @@ -23,7 +22,6 @@ import 'package:provider/provider.dart'; class CoveredFilterChip extends StatelessWidget { final T filter; final double extent, thumbnailExtent; - final AvesEntry? coverEntry; final bool showText, pinned; final String? banner; final FilterCallback? onTap; @@ -34,7 +32,6 @@ class CoveredFilterChip extends StatelessWidget { required this.filter, required this.extent, double? thumbnailExtent, - this.coverEntry, this.showText = true, this.pinned = false, this.banner, @@ -101,7 +98,7 @@ class CoveredFilterChip extends StatelessWidget { } Widget _buildChip(BuildContext context, CollectionSource source) { - final entry = coverEntry ?? source.coverEntry(filter); + final entry = source.coverEntry(filter); final titlePadding = min(4.0, extent / 32); Key? chipKey; if (filter is AlbumFilter) { @@ -170,7 +167,7 @@ class CoveredFilterChip extends StatelessWidget { ); } - Color _detailColor(BuildContext context) => Theme.of(context).textTheme.caption!.color!; + Color _detailColor(BuildContext context) => Theme.of(context).textTheme.bodySmall!.color!; Widget _buildDetails(BuildContext context, CollectionSource source, T filter) { final locale = context.l10n.localeName; diff --git a/lib/widgets/filter_grids/common/enums.dart b/lib/widgets/filter_grids/common/enums.dart new file mode 100644 index 000000000..dca3f4a68 --- /dev/null +++ b/lib/widgets/filter_grids/common/enums.dart @@ -0,0 +1,63 @@ +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 } + +extension ExtraAlbumImportance on AlbumImportance { + String getText(BuildContext context) { + switch (this) { + case AlbumImportance.newAlbum: + return context.l10n.albumTierNew; + case AlbumImportance.pinned: + return context.l10n.albumTierPinned; + case AlbumImportance.special: + return context.l10n.albumTierSpecial; + case AlbumImportance.apps: + return context.l10n.albumTierApps; + case AlbumImportance.regular: + return context.l10n.albumTierRegular; + } + } + + IconData getIcon() { + switch (this) { + case AlbumImportance.newAlbum: + return AIcons.newTier; + case AlbumImportance.pinned: + return AIcons.pin; + case AlbumImportance.special: + return AIcons.important; + case AlbumImportance.apps: + return AIcons.app; + case AlbumImportance.regular: + return AIcons.album; + } + } +} + +enum AlbumMimeType { images, videos, mixed } + +extension ExtraAlbumMimeType on AlbumMimeType { + String getText(BuildContext context) { + switch (this) { + case AlbumMimeType.images: + return context.l10n.drawerCollectionImages; + case AlbumMimeType.videos: + return context.l10n.drawerCollectionVideos; + case AlbumMimeType.mixed: + return context.l10n.albumMimeTypeMixed; + } + } + + IconData getIcon() { + switch (this) { + case AlbumMimeType.images: + return AIcons.image; + case AlbumMimeType.videos: + return AIcons.video; + case AlbumMimeType.mixed: + return AIcons.mimeType; + } + } +} diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index b93899747..92e3183bb 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -6,7 +6,9 @@ import 'package:aves/model/highlight.dart'; import 'package:aves/model/query.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/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'; @@ -15,7 +17,8 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/grid/item_tracker.dart'; import 'package:aves/widgets/common/grid/scaling.dart'; -import 'package:aves/widgets/common/grid/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/fixed/scale_grid.dart'; +import 'package:aves/widgets/common/grid/sections/list_layout.dart'; import 'package:aves/widgets/common/grid/selector.dart'; import 'package:aves/widgets/common/grid/sliver.dart'; import 'package:aves/widgets/common/grid/theme.dart'; @@ -24,6 +27,7 @@ import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/query_provider.dart'; import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart'; +import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:aves/widgets/filter_grids/common/covered_filter_chip.dart'; import 'package:aves/widgets/filter_grids/common/draggable_thumb_label.dart'; @@ -244,6 +248,7 @@ class _FilterGridContent extends StatelessWidget { @override Widget build(BuildContext context) { + final source = context.read(); final settingsRouteKey = context.read().settingsRouteKey; final tileLayout = context.select((s) => s.getTileLayout(settingsRouteKey)); return Selector( @@ -284,7 +289,7 @@ class _FilterGridContent extends StatelessWidget { final tileHeight = CoveredFilterChip.tileHeight( extent: thumbnailExtent, textScaleFactor: textScaleFactor, - showText: tileLayout == TileLayout.grid, + showText: tileLayout != TileLayout.list, ); return GridTheme( extent: thumbnailExtent, @@ -301,17 +306,22 @@ class _FilterGridContent extends StatelessWidget { horizontalPadding: horizontalPadding, tileWidth: thumbnailExtent, tileHeight: tileHeight, - tileBuilder: (gridItem) { + tileBuilder: (gridItem, tileSize) { + final extent = tileSize.shortestSide; return InteractiveFilterTile( gridItem: gridItem, - chipExtent: thumbnailExtent, - thumbnailExtent: thumbnailExtent, + chipExtent: extent, + thumbnailExtent: extent, tileLayout: tileLayout, banner: _getFilterBanner(context, gridItem.filter), heroType: heroType, ); }, tileAnimationDelay: tileAnimationDelay, + coverRatioResolver: (item) { + final coverEntry = source.coverEntry(item.filter) ?? item.entry; + return coverEntry?.displayAspectRatio ?? 1; + }, child: child!, ), ), @@ -469,12 +479,13 @@ class _FilterScaler extends StatelessWidget { final metrics = context.select>((v) => Tuple2(v.spacing, v.horizontalPadding)); final tileSpacing = metrics.item1; final horizontalPadding = metrics.item2; + final brightness = Theme.of(context).brightness; return GridScaleGestureDetector>( scrollableKey: scrollableKey, tileLayout: tileLayout, heightForWidth: (width) => CoveredFilterChip.tileHeight(extent: width, textScaleFactor: textScaleFactor, showText: true), gridBuilder: (center, tileSize, child) => CustomPaint( - painter: GridPainter( + painter: FixedExtentGridPainter( tileLayout: tileLayout, tileCenter: center, tileSize: tileSize, @@ -487,7 +498,7 @@ class _FilterScaler extends StatelessWidget { ), child: child, ), - scaledBuilder: (item, tileSize) => FilterListDetailsTheme( + scaledItemBuilder: (item, tileSize) => FilterListDetailsTheme( extent: tileSize.height, child: FilterTile( gridItem: item, @@ -497,6 +508,16 @@ class _FilterScaler extends StatelessWidget { banner: bannerBuilder(context, item.filter), ), ), + mosaicItemBuilder: (index, targetExtent) => DecoratedBox( + decoration: BoxDecoration( + color: ThumbnailImage.computeLoadingBackgroundColor(index * 10, brightness).withOpacity(.9), + border: Border.all( + color: context.read().neutral, + width: AvesFilterChip.outlineWidth, + ), + borderRadius: BorderRadius.all(CoveredFilterChip.radius(targetExtent)), + ), + ), highlightItem: (item) => item.filter, child: child, ); diff --git a/lib/widgets/filter_grids/common/filter_tile.dart b/lib/widgets/filter_grids/common/filter_tile.dart index 00a0685b8..26981ca5b 100644 --- a/lib/widgets/filter_grids/common/filter_tile.dart +++ b/lib/widgets/filter_grids/common/filter_tile.dart @@ -139,6 +139,7 @@ class FilterTile extends StatelessWidget { final onChipTap = onTap != null ? (filter) => onTap?.call() : null; switch (tileLayout) { + case TileLayout.mosaic: case TileLayout.grid: return FilterChipGridDecorator>( gridItem: gridItem, diff --git a/lib/widgets/filter_grids/common/list_details_theme.dart b/lib/widgets/filter_grids/common/list_details_theme.dart index 1c0218661..774faff0f 100644 --- a/lib/widgets/filter_grids/common/list_details_theme.dart +++ b/lib/widgets/filter_grids/common/list_details_theme.dart @@ -33,9 +33,9 @@ class FilterListDetailsTheme extends StatelessWidget { final textScaleFactor = mq.textScaleFactor; final textTheme = Theme.of(context).textTheme; - final titleStyleBase = textTheme.bodyText2!; + final titleStyleBase = textTheme.bodyMedium!; final titleStyle = titleStyleBase.copyWith(fontSize: titleStyleBase.fontSize! * textScaleFactor); - final captionStyle = textTheme.caption!; + final captionStyle = textTheme.bodySmall!; final titleIconSize = AvesFilterChip.iconSize * textScaleFactor; final titleLineHeight = (RenderParagraph( diff --git a/lib/widgets/filter_grids/common/section_keys.dart b/lib/widgets/filter_grids/common/section_keys.dart index d6dec813e..5f700f9c1 100644 --- a/lib/widgets/filter_grids/common/section_keys.dart +++ b/lib/widgets/filter_grids/common/section_keys.dart @@ -2,6 +2,7 @@ import 'package:aves/model/source/section_keys.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/filter_grids/common/enums.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; @@ -37,38 +38,19 @@ class AlbumImportanceSectionKey extends ChipSectionKey { Widget get leading => Icon(importance.getIcon()); } -enum AlbumImportance { newAlbum, pinned, special, apps, regular } +class MimeTypeSectionKey extends ChipSectionKey { + final AlbumMimeType mimeType; -extension ExtraAlbumImportance on AlbumImportance { - String getText(BuildContext context) { - switch (this) { - case AlbumImportance.newAlbum: - return context.l10n.albumTierNew; - case AlbumImportance.pinned: - return context.l10n.albumTierPinned; - case AlbumImportance.special: - return context.l10n.albumTierSpecial; - case AlbumImportance.apps: - return context.l10n.albumTierApps; - case AlbumImportance.regular: - return context.l10n.albumTierRegular; - } - } + MimeTypeSectionKey._private(BuildContext context, this.mimeType) : super(title: mimeType.getText(context)); - IconData getIcon() { - switch (this) { - case AlbumImportance.newAlbum: - return AIcons.newTier; - case AlbumImportance.pinned: - return AIcons.pin; - case AlbumImportance.special: - return AIcons.important; - case AlbumImportance.apps: - return AIcons.app; - case AlbumImportance.regular: - return AIcons.album; - } - } + factory MimeTypeSectionKey.images(BuildContext context) => MimeTypeSectionKey._private(context, AlbumMimeType.images); + + factory MimeTypeSectionKey.videos(BuildContext context) => MimeTypeSectionKey._private(context, AlbumMimeType.videos); + + factory MimeTypeSectionKey.mixed(BuildContext context) => MimeTypeSectionKey._private(context, AlbumMimeType.mixed); + + @override + Widget get leading => Icon(mimeType.getIcon()); } class StorageVolumeSectionKey extends ChipSectionKey { diff --git a/lib/widgets/filter_grids/common/section_layout.dart b/lib/widgets/filter_grids/common/section_layout.dart index a00bd9c95..c275b9edb 100644 --- a/lib/widgets/filter_grids/common/section_layout.dart +++ b/lib/widgets/filter_grids/common/section_layout.dart @@ -1,6 +1,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/section_keys.dart'; -import 'package:aves/widgets/common/grid/section_layout.dart'; +import 'package:aves/widgets/common/grid/sections/provider.dart'; import 'package:aves/widgets/filter_grids/common/section_header.dart'; import 'package:aves/widgets/filter_grids/common/section_keys.dart'; import 'package:flutter/material.dart'; @@ -22,6 +22,7 @@ class SectionedFilterListLayoutProvider extends Sect required super.tileHeight, required super.tileBuilder, required super.tileAnimationDelay, + required super.coverRatioResolver, required super.child, }); diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 61b363e7f..0eee4039a 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -95,6 +95,9 @@ class _HomePageState extends State { // hide in some countries apps that force quit on permission denial await [ Permission.storage, + // for media access on Android >=13 + Permission.photos, + Permission.videos, // to access media with unredacted metadata with scoped storage (Android >=10) Permission.accessMediaLocation, ].request(); @@ -117,7 +120,12 @@ class _HomePageState extends State { String? uri, mimeType; final widgetId = intentData[intentDataKeyWidgetId]; if (widgetId != null) { - uri = settings.getWidgetUri(widgetId); + // widget settings may be modified in a different process after channel setup + await settings.reload(); + final page = settings.getWidgetOpenPage(widgetId); + if (page == WidgetOpenPage.viewer) { + uri = settings.getWidgetUri(widgetId); + } unawaited(WidgetService.update(widgetId)); } else { uri = intentData[intentDataKeyUri]; diff --git a/lib/widgets/map/map_info_row.dart b/lib/widgets/map/map_info_row.dart index 2ae6b55ce..1bef21f85 100644 --- a/lib/widgets/map/map_info_row.dart +++ b/lib/widgets/map/map_info_row.dart @@ -110,7 +110,7 @@ class _AddressRowState extends State<_AddressRow> { // addresses can include non-latin scripts with inconsistent line height, // which is especially an issue for relayout/painting of heavy Google map, // so we give extra height to give breathing room to the text and stabilize layout - height: Theme.of(context).textTheme.bodyText2!.fontSize! * context.select((mq) => mq.textScaleFactor) * 2, + height: Theme.of(context).textTheme.bodyMedium!.fontSize! * context.select((mq) => mq.textScaleFactor) * 2, child: ValueListenableBuilder( valueListenable: _addressLineNotifier, builder: (context, addressLine, child) { diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 464c7b74e..c59574814 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -21,6 +21,7 @@ import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/common/providers/map_theme_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/thumbnail/scroller.dart'; +import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/map/map_info_row.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/notifications.dart'; @@ -164,9 +165,15 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin @override Widget build(BuildContext context) { - return NotificationListener( + return NotificationListener( onNotification: (notification) { - _goToCollection(notification.filter); + if (notification is FilterSelectedNotification) { + _goToCollection(notification.filter); + } else if (notification is ReverseFilterNotification) { + _goToCollection(notification.reversedFilter); + } else { + return false; + } return true; }, child: Selector( diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index 0b23308ab..c9ed92b34 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -22,6 +22,7 @@ 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'; import 'package:aves/widgets/common/search/delegate.dart'; +import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -65,52 +66,60 @@ class CollectionSearchDelegate extends AvesSearchDelegate { final upQuery = query.trim().toUpperCase(); bool containQuery(String s) => s.toUpperCase().contains(upQuery); return SafeArea( - child: ValueListenableBuilder( + child: NotificationListener( + onNotification: (notification) { + _select(context, notification.reversedFilter); + return true; + }, + child: ValueListenableBuilder( valueListenable: _expandedSectionNotifier, builder: (context, expandedSection, child) { final queryFilter = _buildQueryFilter(false); return Selector>( - selector: (context, s) => s.hiddenFilters, - builder: (context, hiddenFilters, child) { - bool notHidden(CollectionFilter filter) => !hiddenFilters.contains(filter); + selector: (context, s) => s.hiddenFilters, + builder: (context, hiddenFilters, child) { + bool notHidden(CollectionFilter filter) => !hiddenFilters.contains(filter); - final visibleTypeFilters = typeFilters.where(notHidden).toList(); - if (hiddenFilters.contains(MimeFilter.video)) { - [MimeFilter.image, TypeFilter.sphericalVideo].forEach(visibleTypeFilters.remove); - } + final visibleTypeFilters = typeFilters.where(notHidden).toList(); + if (hiddenFilters.contains(MimeFilter.video)) { + [MimeFilter.image, TypeFilter.sphericalVideo].forEach(visibleTypeFilters.remove); + } - final history = settings.searchHistory.where(notHidden).toList(); + final history = settings.searchHistory.where(notHidden).toList(); - return ListView( - padding: const EdgeInsets.only(top: 8), - children: [ + return ListView( + padding: const EdgeInsets.only(top: 8), + children: [ + _buildFilterRow( + context: context, + filters: [ + queryFilter, + ...visibleTypeFilters, + ].whereNotNull().where((f) => containQuery(f.getLabel(context))).toList(), + // usually perform hero animation only on tapped chips, + // but we also need to animate the query chip when it is selected by submitting the search query + heroTypeBuilder: (filter) => filter == queryFilter ? HeroType.always : HeroType.onTap, + ), + if (upQuery.isEmpty && history.isNotEmpty) _buildFilterRow( context: context, - filters: [ - queryFilter, - ...visibleTypeFilters, - ].whereNotNull().where((f) => containQuery(f.getLabel(context))).toList(), - // usually perform hero animation only on tapped chips, - // but we also need to animate the query chip when it is selected by submitting the search query - heroTypeBuilder: (filter) => filter == queryFilter ? HeroType.always : HeroType.onTap, + title: context.l10n.searchRecentSectionTitle, + filters: history, ), - if (upQuery.isEmpty && history.isNotEmpty) - _buildFilterRow( - context: context, - title: context.l10n.searchRecentSectionTitle, - filters: history, - ), - _buildDateFilters(context, containQuery), - _buildAlbumFilters(containQuery), - _buildCountryFilters(containQuery), - _buildPlaceFilters(containQuery), - _buildTagFilters(containQuery), - _buildRatingFilters(context, containQuery), - _buildMetadataFilters(context, containQuery), - ], - ); - }); - }), + _buildDateFilters(context, containQuery), + _buildAlbumFilters(containQuery), + _buildCountryFilters(containQuery), + _buildPlaceFilters(containQuery), + _buildTagFilters(containQuery), + _buildRatingFilters(context, containQuery), + _buildMetadataFilters(context, containQuery), + ], + ); + }, + ); + }, + ), + ), ); } @@ -219,7 +228,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate { MissingFilter.date, LocationFilter(LocationLevel.place, ''), TagFilter(''), - const RatingFilter(0), + RatingFilter(0), MissingFilter.title, ].where((f) => containQuery(f.getLabel(context))).toList(), ); diff --git a/lib/widgets/settings/accessibility/time_to_take_action.dart b/lib/widgets/settings/accessibility/time_to_take_action.dart index 28de8e12f..950eb76ec 100644 --- a/lib/widgets/settings/accessibility/time_to_take_action.dart +++ b/lib/widgets/settings/accessibility/time_to_take_action.dart @@ -37,7 +37,6 @@ class _TimeToTakeActionTileState extends State { selector: (context, s) => s.timeToTakeAction, onSelection: (v) => settings.timeToTakeAction = v, tileTitle: context.l10n.settingsTimeToTakeActionTile, - dialogTitle: context.l10n.settingsTimeToTakeActionDialogTitle, ); }, ); diff --git a/lib/widgets/settings/common/collection_tile.dart b/lib/widgets/settings/common/collection_tile.dart index ef68ab0e1..abbe133c8 100644 --- a/lib/widgets/settings/common/collection_tile.dart +++ b/lib/widgets/settings/common/collection_tile.dart @@ -39,12 +39,12 @@ class SettingsCollectionTile extends StatelessWidget { children: [ Text( l10n.settingsCollectionTile, - style: textTheme.subtitle1!, + style: textTheme.titleMedium!, ), if (hasSubtitle) Text( l10n.drawerCollectionAll, - style: textTheme.bodyText2!.copyWith(color: textTheme.caption!.color), + style: textTheme.bodyMedium!.copyWith(color: textTheme.bodySmall!.color), ), ], ), diff --git a/lib/widgets/settings/common/quick_actions/action_button.dart b/lib/widgets/settings/common/quick_actions/action_button.dart index 9e43a23d6..2d0b2d92d 100644 --- a/lib/widgets/settings/common/quick_actions/action_button.dart +++ b/lib/widgets/settings/common/quick_actions/action_button.dart @@ -1,5 +1,6 @@ import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; class ActionButton extends StatelessWidget { final String text; @@ -14,13 +15,14 @@ class ActionButton extends StatelessWidget { this.showCaption = true, }); - static const padding = 8.0; + static const int maxLines = 2; + static const double padding = 8; @override Widget build(BuildContext context) { - final textStyle = Theme.of(context).textTheme.caption; + final textStyle = _textStyle(context); return SizedBox( - width: OverlayButton.getSize(context) + padding * 2, + width: _width(context), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -35,10 +37,10 @@ class ActionButton extends StatelessWidget { const SizedBox(height: padding), Text( text, - style: enabled ? textStyle : textStyle!.copyWith(color: textStyle.color!.withOpacity(.2)), + style: enabled ? textStyle : textStyle.copyWith(color: textStyle.color!.withOpacity(.2)), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, - maxLines: 2, + maxLines: maxLines, ), ], const SizedBox(height: padding), @@ -46,4 +48,23 @@ class ActionButton extends StatelessWidget { ), ); } + + static TextStyle _textStyle(BuildContext context) => Theme.of(context).textTheme.bodySmall!; + + static double _width(BuildContext context) => OverlayButton.getSize(context) + padding * 2; + + static Size getSize(BuildContext context, String text, {required bool showCaption}) { + final width = _width(context); + var height = width; + if (showCaption) { + final para = RenderParagraph( + TextSpan(text: text, style: _textStyle(context)), + textDirection: TextDirection.ltr, + textScaleFactor: MediaQuery.textScaleFactorOf(context), + maxLines: maxLines, + )..layout(const BoxConstraints(), parentUsesSize: true); + height += para.getMaxIntrinsicHeight(width) + padding; + } + return Size(width, height); + } } diff --git a/lib/widgets/settings/common/quick_actions/action_panel.dart b/lib/widgets/settings/common/quick_actions/action_panel.dart index d2b9cd621..2b099bbbd 100644 --- a/lib/widgets/settings/common/quick_actions/action_panel.dart +++ b/lib/widgets/settings/common/quick_actions/action_panel.dart @@ -13,7 +13,12 @@ class ActionPanel extends StatelessWidget { @override Widget build(BuildContext context) { - final color = highlight ? Theme.of(context).colorScheme.secondary : Colors.blueGrey; + final theme = Theme.of(context); + final color = highlight + ? theme.colorScheme.secondary + : theme.brightness == Brightness.dark + ? Colors.blueGrey + : Colors.blueGrey.shade100; return AnimatedContainer( foregroundDecoration: BoxDecoration( color: color.withOpacity(.2), diff --git a/lib/widgets/settings/common/quick_actions/available_actions.dart b/lib/widgets/settings/common/quick_actions/available_actions.dart index 307ba6fb3..9d5112d99 100644 --- a/lib/widgets/settings/common/quick_actions/available_actions.dart +++ b/lib/widgets/settings/common/quick_actions/available_actions.dart @@ -2,6 +2,7 @@ import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/settings/common/quick_actions/action_button.dart'; import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -15,6 +16,9 @@ class AvailableActionPanel extends StatelessWidget { final Widget? Function(T action) actionIcon; final String Function(BuildContext context, T action) actionText; + static const double spacing = 8; + static const padding = EdgeInsets.all(spacing); + const AvailableActionPanel({ super.key, required this.allActions, @@ -46,26 +50,28 @@ class AvailableActionPanel extends StatelessWidget { builder: (context, accepted, rejected) { return AnimatedBuilder( animation: Listenable.merge([quickActionsChangeNotifier, draggedAvailableAction]), - builder: (context, child) => Padding( - padding: const EdgeInsets.all(8), - child: Wrap( - alignment: WrapAlignment.spaceEvenly, - spacing: 8, - runSpacing: 8, - children: allActions.map((action) { - final dragged = action == draggedAvailableAction.value; - final enabled = dragged || !quickActions.contains(action); - var child = _buildActionButton(context, action, enabled: enabled); - if (dragged) { - child = DraggedPlaceholder(child: child); - } - if (enabled) { - child = _buildDraggable(context, action, child); - } - return child; - }).toList(), - ), - ), + builder: (context, child) { + return Padding( + padding: padding, + child: Wrap( + alignment: WrapAlignment.spaceEvenly, + spacing: spacing, + runSpacing: spacing, + children: allActions.map((action) { + final dragged = action == draggedAvailableAction.value; + final enabled = dragged || !quickActions.contains(action); + var child = _buildActionButton(context, action, enabled: enabled); + if (dragged) { + child = DraggedPlaceholder(child: child); + } + if (enabled) { + child = _buildDraggable(context, action, child); + } + return child; + }).toList(), + ), + ); + }, ); }, ); @@ -113,4 +119,16 @@ class AvailableActionPanel extends StatelessWidget { void _setDraggedAvailableAction(T? action) => draggedAvailableAction.value = action; void _setPanelHighlight(bool flag) => panelHighlight.value = flag; + + static double heightFor(BuildContext context, List captions, double width) { + final buttonSizes = captions.map((v) => ActionButton.getSize(context, v, showCaption: true)); + final actionsPerRun = (width - padding.horizontal + spacing) ~/ (buttonSizes.first.width + spacing); + final runCount = (captions.length / actionsPerRun).ceil(); + var height = .0; + for (var i = 0; i < runCount; i++) { + height += buttonSizes.skip(i * actionsPerRun).take(actionsPerRun).map((v) => v.height).max; + } + height += spacing * (runCount - 1) + padding.vertical; + return height; + } } diff --git a/lib/widgets/settings/common/quick_actions/editor_page.dart b/lib/widgets/settings/common/quick_actions/editor_page.dart index 367bb1408..58645446e 100644 --- a/lib/widgets/settings/common/quick_actions/editor_page.dart +++ b/lib/widgets/settings/common/quick_actions/editor_page.dart @@ -12,12 +12,14 @@ import 'package:aves/widgets/settings/common/quick_actions/available_actions.dar import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart'; import 'package:aves/widgets/settings/common/quick_actions/quick_actions.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; class QuickActionEditorPage extends StatelessWidget { final String title, bannerText; - final List allAvailableActions; + final List> allAvailableActions; final Widget? Function(T action) actionIcon; final String Function(BuildContext context, T action) actionText; final List Function() load; @@ -58,7 +60,7 @@ class QuickActionEditorPage extends StatelessWidget { class QuickActionEditorBody extends StatefulWidget { final String bannerText; - final List allAvailableActions; + final List> allAvailableActions; final Widget? Function(T action) actionIcon; final String Function(BuildContext context, T action) actionText; final List Function() load; @@ -87,6 +89,7 @@ class _QuickActionEditorBodyState extends State _quickActionHighlight = ValueNotifier(false); final ValueNotifier _availableActionHighlight = ValueNotifier(false); final AChangeNotifier _quickActionsChangeNotifier = AChangeNotifier(); + final PageController _availableActionPageController = PageController(); // use a flag to prevent quick action target accept/leave when already animating reorder // as dragging a button against axis direction messes index resolution while items pop in and out @@ -119,6 +122,8 @@ class _QuickActionEditorBodyState extends State( placement: QuickActionPlacement.header, panelHighlight: _quickActionHighlight, @@ -222,7 +227,7 @@ class _QuickActionEditorBodyState extends State extends State( - allActions: widget.allAvailableActions, - quickActions: _quickActions, - quickActionsChangeNotifier: _quickActionsChangeNotifier, - panelHighlight: _availableActionHighlight, - draggedQuickAction: _draggedQuickAction, - draggedAvailableAction: _draggedAvailableAction, - removeQuickAction: _removeQuickAction, - actionIcon: widget.actionIcon, - actionText: widget.actionText, + child: LayoutBuilder( + builder: (context, constraints) { + final allAvailableActions = widget.allAvailableActions; + final maxWidth = constraints.maxWidth; + final maxHeight = allAvailableActions + .map((page) => AvailableActionPanel.heightFor( + context, + page.map((v) => widget.actionText(context, v)).toList(), + maxWidth, + )) + .max; + return Column( + children: [ + if (allAvailableActions.length > 1) + Padding( + padding: const EdgeInsets.only(top: 8), + child: SmoothPageIndicator( + controller: _availableActionPageController, + count: allAvailableActions.length, + effect: WormEffect( + dotWidth: 8, + dotHeight: 8, + dotColor: colorScheme.onPrimary.withOpacity(.3), + activeDotColor: colorScheme.secondary, + ), + ), + ), + SizedBox( + height: maxHeight, + child: PageView( + controller: _availableActionPageController, + children: allAvailableActions + .map((allActions) => AvailableActionPanel( + allActions: allActions, + quickActions: _quickActions, + quickActionsChangeNotifier: _quickActionsChangeNotifier, + panelHighlight: _availableActionHighlight, + draggedQuickAction: _draggedQuickAction, + draggedAvailableAction: _draggedAvailableAction, + removeQuickAction: _removeQuickAction, + actionIcon: widget.actionIcon, + actionText: widget.actionText, + )) + .toList(), + ), + ), + ], + ); + }, ), ), ], diff --git a/lib/widgets/settings/common/tiles.dart b/lib/widgets/settings/common/tiles.dart index d12564118..ceab95463 100644 --- a/lib/widgets/settings/common/tiles.dart +++ b/lib/widgets/settings/common/tiles.dart @@ -1,5 +1,6 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/identity/aves_caption.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -82,7 +83,8 @@ class SettingsSelectionListTile extends StatelessWidget { final String Function(BuildContext, T) getName; final T Function(BuildContext, Settings) selector; final ValueChanged onSelection; - final String tileTitle, dialogTitle; + final String tileTitle; + final String? dialogTitle; final TextBuilder? optionSubtitleBuilder; const SettingsSelectionListTile({ @@ -92,7 +94,7 @@ class SettingsSelectionListTile extends StatelessWidget { required this.selector, required this.onSelection, required this.tileTitle, - required this.dialogTitle, + this.dialogTitle, this.optionSubtitleBuilder, }); @@ -102,7 +104,7 @@ class SettingsSelectionListTile extends StatelessWidget { selector: selector, builder: (context, current, child) => ListTile( title: Text(tileTitle), - subtitle: Text(getName(context, current)), + subtitle: AvesCaption(getName(context, current)), onTap: () => showSelectionDialog( context: context, builder: (context) => AvesSelectionDialog( diff --git a/lib/widgets/settings/home_widget_settings_page.dart b/lib/widgets/settings/home_widget_settings_page.dart index 409c95878..3a91a15f3 100644 --- a/lib/widgets/settings/home_widget_settings_page.dart +++ b/lib/widgets/settings/home_widget_settings_page.dart @@ -1,5 +1,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/widget_open_action.dart'; import 'package:aves/model/settings/enums/widget_shape.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/widget_service.dart'; @@ -12,6 +13,7 @@ import 'package:aves/widgets/common/identity/buttons.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/home_widget.dart'; import 'package:aves/widgets/settings/common/collection_tile.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -31,8 +33,9 @@ class HomeWidgetSettingsPage extends StatefulWidget { } class _HomeWidgetSettingsPageState extends State { - late Color? _outline; late WidgetShape _shape; + late Color? _outline; + late WidgetOpenPage _openPage; late Set _collectionFilters; int get widgetId => widget.widgetId; @@ -53,8 +56,9 @@ class _HomeWidgetSettingsPageState extends State { @override void initState() { super.initState(); - _outline = settings.getWidgetOutline(widgetId); _shape = settings.getWidgetShape(widgetId); + _outline = settings.getWidgetOutline(widgetId); + _openPage = settings.getWidgetOpenPage(widgetId); _collectionFilters = settings.getWidgetCollectionFilters(widgetId); } @@ -80,6 +84,13 @@ class _HomeWidgetSettingsPageState extends State { setter: (v) => setState(() => _outline = v), ), ), + SettingsSelectionListTile( + values: WidgetOpenPage.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => _openPage, + onSelection: (v) => setState(() => _openPage = v), + tileTitle: l10n.settingsWidgetOpenPage, + ), SettingsCollectionTile( filters: _collectionFilters, onSelection: (v) => setState(() => _collectionFilters = v), @@ -137,8 +148,9 @@ class _HomeWidgetSettingsPageState extends State { } void _saveSettings() { - settings.setWidgetOutline(widgetId, _outline); settings.setWidgetShape(widgetId, _shape); + settings.setWidgetOutline(widgetId, _outline); + settings.setWidgetOpenPage(widgetId, _openPage); if (!const SetEquality().equals(_collectionFilters, settings.getWidgetCollectionFilters(widgetId))) { settings.setWidgetCollectionFilters(widgetId, _collectionFilters); settings.setWidgetUri(widgetId, null); diff --git a/lib/widgets/settings/privacy/file_picker/crumb_line.dart b/lib/widgets/settings/privacy/file_picker/crumb_line.dart index e38838d64..25c624950 100644 --- a/lib/widgets/settings/privacy/file_picker/crumb_line.dart +++ b/lib/widgets/settings/privacy/file_picker/crumb_line.dart @@ -25,7 +25,7 @@ class _CrumbLineState extends State { @override void didUpdateWidget(covariant CrumbLine oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.directory.relativeDir.length > oldWidget.directory.relativeDir.length) { + if (oldWidget.directory.relativeDir.length < widget.directory.relativeDir.length) { // scroll to show last crumb WidgetsBinding.instance.addPostFrameCallback((_) { final extent = _controller.position.maxScrollExtent; @@ -44,7 +44,7 @@ class _CrumbLineState extends State { directory.getVolumeDescription(context), ...pContext.split(directory.relativeDir), ]; - final crumbStyle = Theme.of(context).textTheme.bodyText2; + final crumbStyle = Theme.of(context).textTheme.bodyMedium; final crumbColor = crumbStyle!.color!.withOpacity(.4); return DefaultTextStyle( style: crumbStyle.copyWith( diff --git a/lib/widgets/settings/privacy/file_picker/file_picker.dart b/lib/widgets/settings/privacy/file_picker/file_picker.dart index 378a167f3..973809955 100644 --- a/lib/widgets/settings/privacy/file_picker/file_picker.dart +++ b/lib/widgets/settings/privacy/file_picker/file_picker.dart @@ -157,7 +157,7 @@ class _FilePickerState extends State { padding: const EdgeInsets.all(16), child: Text( context.l10n.filePickerOpenFrom, - style: Theme.of(context).textTheme.headline5, + style: Theme.of(context).textTheme.headlineSmall, ), ), ...volumes.map((v) { diff --git a/lib/widgets/settings/screen_saver_settings_page.dart b/lib/widgets/settings/screen_saver_settings_page.dart index e7618c257..217048299 100644 --- a/lib/widgets/settings/screen_saver_settings_page.dart +++ b/lib/widgets/settings/screen_saver_settings_page.dart @@ -32,13 +32,17 @@ class ScreenSaverSettingsPage extends StatelessWidget { onChanged: (v) => settings.screenSaverFillScreen = v, title: context.l10n.settingsSlideshowFillScreen, ), + SettingsSwitchListTile( + selector: (context, s) => s.screenSaverAnimatedZoomEffect, + onChanged: (v) => settings.screenSaverAnimatedZoomEffect = v, + title: context.l10n.settingsSlideshowAnimatedZoomEffect, + ), SettingsSelectionListTile( values: ViewerTransition.values, getName: (context, v) => v.getName(context), selector: (context, s) => s.screenSaverTransition, onSelection: (v) => settings.screenSaverTransition = v, tileTitle: l10n.settingsSlideshowTransitionTile, - dialogTitle: l10n.settingsSlideshowTransitionDialogTitle, ), SettingsSelectionListTile( values: SlideshowInterval.values, @@ -46,7 +50,6 @@ class ScreenSaverSettingsPage extends StatelessWidget { selector: (context, s) => s.screenSaverInterval, onSelection: (v) => settings.screenSaverInterval = v, tileTitle: l10n.settingsSlideshowIntervalTile, - dialogTitle: l10n.settingsSlideshowIntervalDialogTitle, ), SettingsSelectionListTile( values: SlideshowVideoPlayback.values, diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index 2f7a3e685..52e17402f 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -105,7 +105,7 @@ class _SettingsPageState extends State with FeedbackMixin { data: theme.copyWith( textTheme: theme.textTheme.copyWith( // dense style font for tile subtitles, without modifying title font - bodyText2: const TextStyle(fontSize: 12), + bodyMedium: const TextStyle(fontSize: 12), ), ), child: AnimationLimiter( diff --git a/lib/widgets/settings/thumbnails/collection_actions_editor.dart b/lib/widgets/settings/thumbnails/collection_actions_editor.dart index 5bacb4ce5..38303fe73 100644 --- a/lib/widgets/settings/thumbnails/collection_actions_editor.dart +++ b/lib/widgets/settings/thumbnails/collection_actions_editor.dart @@ -19,7 +19,7 @@ class CollectionActionEditorPage extends StatelessWidget { Tab(text: l10n.settingsCollectionQuickActionTabBrowsing), QuickActionEditorBody( bannerText: context.l10n.settingsCollectionBrowsingQuickActionEditorBanner, - allAvailableActions: EntrySetActions.collectionEditorBrowsing, + allAvailableActions: const [EntrySetActions.collectionEditorBrowsing], actionIcon: (action) => action.getIcon(), actionText: (context, action) => action.getText(context), load: () => settings.collectionBrowsingQuickActions, @@ -30,7 +30,10 @@ class CollectionActionEditorPage extends StatelessWidget { Tab(text: l10n.settingsCollectionQuickActionTabSelecting), QuickActionEditorBody( bannerText: context.l10n.settingsCollectionSelectionQuickActionEditorBanner, - allAvailableActions: EntrySetActions.collectionEditorSelection, + allAvailableActions: const [ + EntrySetActions.collectionEditorSelectionRegular, + EntrySetActions.collectionEditorSelectionEdit, + ], actionIcon: (action) => action.getIcon(), actionText: (context, action) => action.getText(context), load: () => settings.collectionSelectionQuickActions, diff --git a/lib/widgets/settings/video/controls.dart b/lib/widgets/settings/video/controls.dart index cee40f19e..e57b8ef18 100644 --- a/lib/widgets/settings/video/controls.dart +++ b/lib/widgets/settings/video/controls.dart @@ -25,7 +25,6 @@ class VideoControlsPage extends StatelessWidget { selector: (context, s) => s.videoControls, onSelection: (v) => settings.videoControls = v, tileTitle: context.l10n.settingsVideoButtonsTile, - dialogTitle: context.l10n.settingsVideoButtonsDialogTitle, ), SettingsSwitchListTile( selector: (context, s) => s.videoGestureDoubleTapTogglePlay, diff --git a/lib/widgets/settings/video/video.dart b/lib/widgets/settings/video/video.dart index d1711ac51..1264c82c3 100644 --- a/lib/widgets/settings/video/video.dart +++ b/lib/widgets/settings/video/video.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/video_auto_play_mode.dart'; import 'package:aves/model/settings/enums/video_loop_mode.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; @@ -73,13 +74,15 @@ class SettingsTileVideoEnableHardwareAcceleration extends SettingsTile { class SettingsTileVideoEnableAutoPlay extends SettingsTile { @override - String title(BuildContext context) => context.l10n.settingsVideoEnableAutoPlay; + String title(BuildContext context) => context.l10n.settingsVideoAutoPlay; @override - Widget build(BuildContext context) => SettingsSwitchListTile( - selector: (context, s) => s.enableVideoAutoPlay, - onChanged: (v) => settings.enableVideoAutoPlay = v, - title: title(context), + Widget build(BuildContext context) => SettingsSelectionListTile( + values: VideoAutoPlayMode.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.videoAutoPlayMode, + onSelection: (v) => settings.videoAutoPlayMode = v, + tileTitle: title(context), ); } diff --git a/lib/widgets/settings/video/video_settings_page.dart b/lib/widgets/settings/video/video_settings_page.dart index 4567144ec..edacc22d4 100644 --- a/lib/widgets/settings/video/video_settings_page.dart +++ b/lib/widgets/settings/video/video_settings_page.dart @@ -28,7 +28,7 @@ class _VideoSettingsPageState extends State { data: theme.copyWith( textTheme: theme.textTheme.copyWith( // dense style font for tile subtitles, without modifying title font - bodyText2: const TextStyle(fontSize: 12), + bodyMedium: const TextStyle(fontSize: 12), ), ), child: SafeArea( diff --git a/lib/widgets/settings/viewer/slideshow.dart b/lib/widgets/settings/viewer/slideshow.dart index a266d13f3..1c83f9f63 100644 --- a/lib/widgets/settings/viewer/slideshow.dart +++ b/lib/widgets/settings/viewer/slideshow.dart @@ -36,13 +36,17 @@ class ViewerSlideshowPage extends StatelessWidget { onChanged: (v) => settings.slideshowFillScreen = v, title: context.l10n.settingsSlideshowFillScreen, ), + SettingsSwitchListTile( + selector: (context, s) => s.slideshowAnimatedZoomEffect, + onChanged: (v) => settings.slideshowAnimatedZoomEffect = v, + title: context.l10n.settingsSlideshowAnimatedZoomEffect, + ), SettingsSelectionListTile( values: ViewerTransition.values, getName: (context, v) => v.getName(context), selector: (context, s) => s.slideshowTransition, onSelection: (v) => settings.slideshowTransition = v, tileTitle: context.l10n.settingsSlideshowTransitionTile, - dialogTitle: context.l10n.settingsSlideshowTransitionDialogTitle, ), SettingsSelectionListTile( values: SlideshowInterval.values, @@ -50,7 +54,6 @@ class ViewerSlideshowPage extends StatelessWidget { selector: (context, s) => s.slideshowInterval, onSelection: (v) => settings.slideshowInterval = v, tileTitle: context.l10n.settingsSlideshowIntervalTile, - dialogTitle: context.l10n.settingsSlideshowIntervalDialogTitle, ), SettingsSelectionListTile( values: SlideshowVideoPlayback.values, diff --git a/lib/widgets/settings/viewer/viewer_actions_editor.dart b/lib/widgets/settings/viewer/viewer_actions_editor.dart index b70b10344..6c18e529d 100644 --- a/lib/widgets/settings/viewer/viewer_actions_editor.dart +++ b/lib/widgets/settings/viewer/viewer_actions_editor.dart @@ -10,22 +10,26 @@ class ViewerActionEditorPage extends StatelessWidget { const ViewerActionEditorPage({super.key}); static const allAvailableActions = [ - EntryAction.share, - EntryAction.edit, - EntryAction.rename, - EntryAction.delete, - EntryAction.copy, - EntryAction.move, - EntryAction.toggleFavourite, - EntryAction.rotateScreen, - EntryAction.videoCaptureFrame, - EntryAction.videoToggleMute, - EntryAction.videoSetSpeed, - EntryAction.videoSelectStreams, - EntryAction.viewSource, - EntryAction.rotateCCW, - EntryAction.rotateCW, - EntryAction.flip, + [ + EntryAction.share, + EntryAction.edit, + EntryAction.rename, + EntryAction.delete, + EntryAction.copy, + EntryAction.move, + EntryAction.toggleFavourite, + EntryAction.rotateScreen, + EntryAction.viewSource, + EntryAction.rotateCCW, + EntryAction.rotateCW, + EntryAction.flip, + ], + [ + EntryAction.videoCaptureFrame, + EntryAction.videoToggleMute, + EntryAction.videoSetSpeed, + EntryAction.videoSelectStreams, + ], ]; @override diff --git a/lib/widgets/stats/date/histogram.dart b/lib/widgets/stats/date/histogram.dart index 032822536..80a73f0b4 100644 --- a/lib/widgets/stats/date/histogram.dart +++ b/lib/widgets/stats/date/histogram.dart @@ -330,7 +330,7 @@ class _HistogramState extends State with AutomaticKeepAliveClientMixi Text( numberFormat.format(count), style: TextStyle( - color: Theme.of(context).textTheme.caption!.color, + color: Theme.of(context).textTheme.bodySmall!.color, ), textAlign: TextAlign.end, ), diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart index 62159b672..d94020345 100644 --- a/lib/widgets/stats/filter_table.dart +++ b/lib/widgets/stats/filter_table.dart @@ -104,7 +104,7 @@ class FilterTable extends StatelessWidget { Text( numberFormat.format(count), style: TextStyle( - color: theme.textTheme.caption!.color, + color: theme.textTheme.bodySmall!.color, ), textAlign: TextAlign.end, ), diff --git a/lib/widgets/stats/mime_donut.dart b/lib/widgets/stats/mime_donut.dart index 249d15f5b..76c633256 100644 --- a/lib/widgets/stats/mime_donut.dart +++ b/lib/widgets/stats/mime_donut.dart @@ -136,7 +136,7 @@ class _MimeDonutState extends State with AutomaticKeepAliveClientMixi Text( numberFormat.format(d.entryCount), style: TextStyle( - color: Theme.of(context).textTheme.caption!.color, + color: Theme.of(context).textTheme.bodySmall!.color, ), ), ], diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index e1479b427..f4bb72074 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -1,6 +1,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/filters/location.dart'; import 'package:aves/model/filters/rating.dart'; @@ -15,8 +16,11 @@ import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/basic/insets.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/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/stats/date/histogram.dart'; import 'package:aves/widgets/stats/filter_table.dart'; import 'package:aves/widgets/stats/mime_donut.dart'; @@ -47,7 +51,7 @@ class StatsPage extends StatefulWidget { } class _StatsPageState extends State { - final Map _entryCountPerCountry = {}, _entryCountPerPlace = {}, _entryCountPerTag = {}; + final Map _entryCountPerCountry = {}, _entryCountPerPlace = {}, _entryCountPerTag = {}, _entryCountPerAlbum = {}; final Map _entryCountPerRating = Map.fromEntries(List.generate(7, (i) => MapEntry(5 - i, 0))); late final ValueNotifier _isPageAnimatingNotifier; @@ -81,6 +85,11 @@ class _StatsPageState extends State { _entryCountPerTag[tag] = (_entryCountPerTag[tag] ?? 0) + 1; }); + final album = entry.directory; + if (album != null) { + _entryCountPerAlbum[album] = (_entryCountPerAlbum[album] ?? 0) + 1; + } + final rating = entry.rating; _entryCountPerRating[rating] = (_entryCountPerRating[rating] ?? 0) + 1; }); @@ -177,30 +186,38 @@ class _StatsPageState extends State { ), ); final showRatings = _entryCountPerRating.entries.any((kv) => kv.key != 0 && kv.value > 0); - child = AnimationLimiter( - child: ListView( - children: AnimationConfiguration.toStaggeredList( - duration: durations.staggeredAnimation, - delay: durations.staggeredAnimationDelay * timeDilation, - childAnimationBuilder: (child) => SlideAnimation( - verticalOffset: 50.0, - child: FadeInAnimation( - child: child, + final source = widget.source; + child = NotificationListener( + onNotification: (notification) { + _onFilterSelection(context, notification.reversedFilter); + return true; + }, + child: AnimationLimiter( + child: ListView( + children: AnimationConfiguration.toStaggeredList( + duration: durations.staggeredAnimation, + delay: durations.staggeredAnimationDelay * timeDilation, + childAnimationBuilder: (child) => SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: child, + ), ), + children: [ + mimeDonuts, + Histogram( + entries: entries, + animationDuration: chartAnimationDuration, + onFilterSelection: (filter) => _onFilterSelection(context, filter), + ), + locationIndicator, + ..._buildFilterSection(context, l10n.statsTopCountriesSectionTitle, _entryCountPerCountry, (v) => LocationFilter(LocationLevel.country, v)), + ..._buildFilterSection(context, l10n.statsTopPlacesSectionTitle, _entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)), + ..._buildFilterSection(context, l10n.statsTopTagsSectionTitle, _entryCountPerTag, TagFilter.new), + ..._buildFilterSection(context, l10n.statsTopAlbumsSectionTitle, _entryCountPerAlbum, (v) => AlbumFilter(v, source.getAlbumDisplayName(context, v))), + if (showRatings) ..._buildFilterSection(context, l10n.searchRatingSectionTitle, _entryCountPerRating, RatingFilter.new, sortByCount: false, maxRowCount: null), + ], ), - children: [ - mimeDonuts, - Histogram( - entries: entries, - animationDuration: chartAnimationDuration, - onFilterSelection: (filter) => _onFilterSelection(context, filter), - ), - locationIndicator, - ..._buildFilterSection(context, l10n.statsTopCountriesSectionTitle, _entryCountPerCountry, (v) => LocationFilter(LocationLevel.country, v)), - ..._buildFilterSection(context, l10n.statsTopPlacesSectionTitle, _entryCountPerPlace, (v) => LocationFilter(LocationLevel.place, v)), - ..._buildFilterSection(context, l10n.statsTopTagsSectionTitle, _entryCountPerTag, TagFilter.new), - if (showRatings) ..._buildFilterSection(context, l10n.searchRatingSectionTitle, _entryCountPerRating, RatingFilter.new, sortByCount: false, maxRowCount: null), - ], ), ), ); @@ -215,7 +232,12 @@ class _StatsPageState extends State { body: GestureAreaProtectorStack( child: SafeArea( bottom: false, - child: child, + child: TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, + ), + child: child, + ), ), ), ), @@ -230,20 +252,51 @@ class _StatsPageState extends State { Map entryCountMap, CollectionFilter Function(T key) filterBuilder, { bool sortByCount = true, - int? maxRowCount = 5, + int? maxRowCount = 3, }) { if (entryCountMap.isEmpty) return []; + final totalEntryCount = entries.length; + final hasMore = maxRowCount != null && entryCountMap.length > maxRowCount; return [ Padding( padding: const EdgeInsets.all(16), - child: Text( - title, - style: Constants.knownTitleTextStyle, + child: Row( + children: [ + Text( + title, + style: Constants.knownTitleTextStyle, + ), + const Spacer(), + IconButton( + icon: const Icon(AIcons.next), + onPressed: hasMore + ? () => Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: StatsTopPage.routeName), + builder: (context) => StatsTopPage( + title: title, + tableBuilder: (context) => FilterTable( + totalEntryCount: totalEntryCount, + entryCountMap: entryCountMap, + filterBuilder: filterBuilder, + sortByCount: sortByCount, + maxRowCount: null, + onFilterSelection: (filter) => _onFilterSelection(context, filter), + ), + onFilterSelection: (filter) => _onFilterSelection(context, filter), + ), + ), + ) + : null, + tooltip: MaterialLocalizations.of(context).moreButtonTooltip, + ), + ], ), ), FilterTable( - totalEntryCount: entries.length, + totalEntryCount: totalEntryCount, entryCountMap: entryCountMap, filterBuilder: filterBuilder, sortByCount: sortByCount, @@ -268,7 +321,7 @@ 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.pop(context)); + Future.delayed(const Duration(milliseconds: 100), () => Navigator.popUntil(context, (route) => route.settings.name == CollectionPage.routeName)); } void _jumpToCollectionPage(BuildContext context, CollectionFilter filter) { @@ -285,3 +338,49 @@ class _StatsPageState extends State { ); } } + +class StatsTopPage extends StatelessWidget { + static const routeName = '/collection/stats/top'; + + final String title; + final WidgetBuilder tableBuilder; + final FilterCallback onFilterSelection; + + const StatsTopPage({ + super.key, + required this.title, + required this.tableBuilder, + required this.onFilterSelection, + }); + + @override + Widget build(BuildContext context) { + return MediaQueryDataProvider( + child: Scaffold( + appBar: AppBar( + title: Text(title), + ), + body: GestureAreaProtectorStack( + child: SafeArea( + bottom: false, + child: Builder(builder: (context) { + return NotificationListener( + onNotification: (notification) { + onFilterSelection(notification.reversedFilter); + return true; + }, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 8) + + EdgeInsets.only( + bottom: context.select((mq) => mq.effectiveBottomPadding), + ), + child: tableBuilder(context), + ), + ); + }), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/viewer/controller.dart b/lib/widgets/viewer/controller.dart index bae8ec799..2353a75fa 100644 --- a/lib/widgets/viewer/controller.dart +++ b/lib/widgets/viewer/controller.dart @@ -1,20 +1,25 @@ import 'dart:async'; +import 'dart:math'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/magnifier/scale/scale_level.dart'; import 'package:flutter/widgets.dart'; class ViewerController { final ValueNotifier entryNotifier = ValueNotifier(null); - final ScaleLevel initialScale; final ViewerTransition transition; final Duration? autopilotInterval; + final bool autopilotAnimatedZoom; final bool repeat; + late final ScaleLevel _initialScale; late final ValueNotifier _autopilotNotifier; Timer? _playTimer; final StreamController _streamController = StreamController.broadcast(); + final Map _autopilotAnimationControllers = {}; + ScaleLevel? _autopilotInitialScale; Stream get _events => _streamController.stream; @@ -26,13 +31,22 @@ class ViewerController { set autopilot(bool enabled) => _autopilotNotifier.value = enabled; + ScaleLevel get initialScale => _autopilotInitialScale ?? _initialScale; + + static final _autopilotScaleTweens = [ + Tween(begin: 1, end: 1.2), + Tween(begin: 1.2, end: 1), + ]; + ViewerController({ - this.initialScale = const ScaleLevel(ref: ScaleReference.contained), + ScaleLevel initialScale = const ScaleLevel(ref: ScaleReference.contained), this.transition = ViewerTransition.parallax, this.repeat = false, bool autopilot = false, this.autopilotInterval, + this.autopilotAnimatedZoom = false, }) { + _initialScale = initialScale; _autopilotNotifier = ValueNotifier(autopilot); _autopilotNotifier.addListener(_onAutopilotChange); _onAutopilotChange(); @@ -40,21 +54,53 @@ class ViewerController { void dispose() { _autopilotNotifier.removeListener(_onAutopilotChange); + _clearAutopilotAnimations(); _stopPlayTimer(); _streamController.close(); } - void _stopPlayTimer() { - _playTimer?.cancel(); - } - void _onAutopilotChange() { + _clearAutopilotAnimations(); _stopPlayTimer(); if (autopilot && autopilotInterval != null) { _playTimer = Timer.periodic(autopilotInterval!, (_) => _streamController.add(ViewerShowNextEvent())); _streamController.add(const ViewerOverlayToggleEvent(visible: false)); } } + + void _stopPlayTimer() => _playTimer?.cancel(); + + void _clearAutopilotAnimations() => _autopilotAnimationControllers.keys.toSet().forEach((v) => stopAutopilotAnimation(vsync: v)); + + void stopAutopilotAnimation({required TickerProvider vsync}) => _autopilotAnimationControllers.remove(vsync)?.dispose(); + + void startAutopilotAnimation({ + required TickerProvider vsync, + required void Function({required ScaleLevel scaleLevel}) onUpdate, + }) { + stopAutopilotAnimation(vsync: vsync); + if (!autopilot || !autopilotAnimatedZoom) return; + + final scaleLevelRef = _initialScale.ref; + final scaleFactorTween = _autopilotScaleTweens[Random().nextInt(_autopilotScaleTweens.length)]; + _autopilotInitialScale = ScaleLevel(ref: scaleLevelRef, factor: scaleFactorTween.begin!); + + final animationController = AnimationController( + duration: autopilotInterval, + vsync: vsync, + ); + animationController.addListener(() => onUpdate.call( + scaleLevel: ScaleLevel( + ref: scaleLevelRef, + factor: scaleFactorTween.evaluate(CurvedAnimation( + parent: animationController, + curve: Curves.linear, + )), + ), + )); + _autopilotAnimationControllers[vsync] = animationController; + Future.delayed(Durations.viewerHorizontalPageAnimation).then((_) => _autopilotAnimationControllers[vsync]?.forward()); + } } @immutable @@ -119,4 +165,26 @@ class PageTransitionEffects { ), ); }; + + static TransitionBuilder none( + PageController pageController, + int index, + ) => + (context, child) { + double opacity = 0; + double dx = 0; + if (pageController.hasClients && pageController.position.haveDimensions) { + final position = (pageController.page! - index).clamp(-1.0, 1.0); + final width = pageController.position.viewportDimension; + opacity = (1 - position.abs()).roundToDouble().clamp(0, 1); + dx = position * width; + } + return Opacity( + opacity: opacity, + child: Transform.translate( + offset: Offset(dx, 0), + child: child, + ), + ); + }; } diff --git a/lib/widgets/viewer/entry_horizontal_pager.dart b/lib/widgets/viewer/entry_horizontal_pager.dart index c184a0a47..67752da83 100644 --- a/lib/widgets/viewer/entry_horizontal_pager.dart +++ b/lib/widgets/viewer/entry_horizontal_pager.dart @@ -90,7 +90,7 @@ class _MultiEntryScrollerState extends State with AutomaticK key: const Key('image_view'), mainEntry: mainEntry, pageEntry: pageEntry ?? mainEntry, - initialScale: viewerController.initialScale, + viewerController: viewerController, onDisposed: () => widget.onViewDisposed(mainEntry, pageEntry), ); } @@ -139,7 +139,7 @@ class _SingleEntryScrollerState extends State with Automati return EntryPageView( mainEntry: mainEntry, pageEntry: pageEntry ?? mainEntry, - initialScale: viewerController.initialScale, + viewerController: widget.viewerController, ); } diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index 16150349e..5a06022f4 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -229,7 +229,7 @@ class _ViewerVerticalPageViewState extends State { if (animate) { pageController.animateToPage( target, - duration: const Duration(seconds: 1), + duration: Durations.viewerHorizontalPageAnimation, curve: Curves.easeInOutCubic, ); } else { diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index 5e599b3a6..b5b61a750 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -189,7 +189,7 @@ class _BasicInfoState extends State<_BasicInfo> { @override void initState() { super.initState(); - if (!entry.trashed && entry.isMediaStoreContent) { + if (!entry.trashed && entry.isMediaStoreMediaContent) { _ownerPackageLoader = metadataFetchService.hasContentResolverProp(ownerPackageNamePropKey).then((exists) { return exists ? metadataFetchService.getContentResolverProp(entry, ownerPackageNamePropKey) : SynchronousFuture(null); }); diff --git a/lib/widgets/viewer/info/common.dart b/lib/widgets/viewer/info/common.dart index 134dda62b..578a3439d 100644 --- a/lib/widgets/viewer/info/common.dart +++ b/lib/widgets/viewer/info/common.dart @@ -50,7 +50,7 @@ class InfoRowGroup extends StatefulWidget { static const valueStyle = TextStyle(fontSize: fontSize); static final _keyStyle = valueStyle.copyWith(height: 2.0); - static TextStyle keyStyle(BuildContext context) => Theme.of(context).textTheme.caption!.merge(_keyStyle); + static TextStyle keyStyle(BuildContext context) => Theme.of(context).textTheme.bodySmall!.merge(_keyStyle); const InfoRowGroup({ super.key, diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index f2a2a5a21..31a4e8a96 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -8,6 +8,7 @@ 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/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart'; import 'package:aves/widgets/viewer/embedded/embedded_data_opener.dart'; import 'package:aves/widgets/viewer/info/basic_section.dart'; @@ -230,25 +231,31 @@ class _InfoPageContentState extends State<_InfoPageContent> { metadataNotifier: _metadataNotifier, ); - return CustomScrollView( - controller: widget.scrollController, - slivers: [ - InfoAppBar( - entry: entry, - actionDelegate: _actionDelegate, - metadataNotifier: _metadataNotifier, - onBackPressed: widget.goToViewer, - ), - SliverPadding( - padding: horizontalPadding + const EdgeInsets.only(top: 8), - sliver: basicAndLocationSliver, - ), - SliverPadding( - padding: horizontalPadding + const EdgeInsets.only(bottom: 8), - sliver: metadataSliver, - ), - const BottomPaddingSliver(), - ], + return NotificationListener( + onNotification: (notification) { + _onFilter(notification.reversedFilter); + return true; + }, + child: CustomScrollView( + controller: widget.scrollController, + slivers: [ + InfoAppBar( + entry: entry, + actionDelegate: _actionDelegate, + metadataNotifier: _metadataNotifier, + onBackPressed: widget.goToViewer, + ), + SliverPadding( + padding: horizontalPadding + const EdgeInsets.only(top: 8), + sliver: basicAndLocationSliver, + ), + SliverPadding( + padding: horizontalPadding + const EdgeInsets.only(bottom: 8), + sliver: metadataSliver, + ), + const BottomPaddingSliver(), + ], + ), ); } diff --git a/lib/widgets/viewer/info/info_search.dart b/lib/widgets/viewer/info/info_search.dart index bd2018b54..8e9f4022c 100644 --- a/lib/widgets/viewer/info/info_search.dart +++ b/lib/widgets/viewer/info/info_search.dart @@ -24,7 +24,7 @@ class InfoSearchDelegate extends SearchDelegate { ); @override - Widget buildLeading(BuildContext context) { + Widget? buildLeading(BuildContext context) { return IconButton( icon: AnimatedIcon( icon: AnimatedIcons.menu_arrow, @@ -36,7 +36,7 @@ class InfoSearchDelegate extends SearchDelegate { } @override - List buildActions(BuildContext context) { + List? buildActions(BuildContext context) { return [ if (query.isNotEmpty) IconButton( diff --git a/lib/widgets/viewer/info/metadata/xmp_card.dart b/lib/widgets/viewer/info/metadata/xmp_card.dart new file mode 100644 index 000000000..5afd950b0 --- /dev/null +++ b/lib/widgets/viewer/info/metadata/xmp_card.dart @@ -0,0 +1,157 @@ +import 'dart:math'; + +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/basic/multi_cross_fader.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/highlight_title.dart'; +import 'package:aves/widgets/viewer/info/common.dart'; +import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:tuple/tuple.dart'; + +typedef XmpExtractedCard = Tuple2, List?>; + +class XmpCard extends StatefulWidget { + final String title; + late final XmpExtractedCard? directStruct; + late final List? indexedStructs; + final String Function(XmpProp prop) formatValue; + final Map Function(int? index)? spanBuilders; + + XmpCard({ + super.key, + required this.title, + required Map structByIndex, + required this.formatValue, + this.spanBuilders, + }) { + directStruct = structByIndex[null]; + + final length = structByIndex.keys.whereNotNull().fold(0, max); + indexedStructs = length > 0 ? [for (var i = 0; i < length; i++) structByIndex[i + 1] ?? const Tuple2({}, null)] : null; + } + + @override + State createState() => _XmpCardState(); +} + +class _XmpCardState extends State { + final ValueNotifier _indexNotifier = ValueNotifier(0); + + List? get indexedStructs => widget.indexedStructs; + + bool get isIndexed => indexedStructs != null; + + int get indexedStructCount => indexedStructs?.length ?? 0; + + @override + void initState() { + super.initState(); + if (isIndexed) { + _indexNotifier.value = indexedStructCount - 1; + } + } + + @override + void didUpdateWidget(covariant XmpCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (_indexNotifier.value >= indexedStructCount) { + _indexNotifier.value = indexedStructCount - 1; + } + } + + @override + Widget build(BuildContext context) { + final _isIndexed = isIndexed; + + return Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.onPrimary.withOpacity(.2), + ), + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + child: ValueListenableBuilder( + valueListenable: _indexNotifier, + builder: (context, index, child) { + final data = _isIndexed ? indexedStructs![index] : widget.directStruct!; + final props = data.item1.entries.map((kv) => XmpProp(kv.key, kv.value.value)).toList()..sort(); + final cards = data.item2; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8, top: 8, right: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: HighlightTitle( + title: widget.title, + selectable: true, + showHighlight: false, + ), + ), + if (_isIndexed) ...[ + IconButton( + visualDensity: VisualDensity.compact, + icon: const Icon(AIcons.previous), + onPressed: index > 0 ? () => _setIndex(index - 1) : null, + tooltip: context.l10n.previousTooltip, + ), + HighlightTitle( + title: '${index + 1}', + showHighlight: false, + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: const Icon(AIcons.next), + onPressed: index < indexedStructCount - 1 ? () => _setIndex(index + 1) : null, + tooltip: context.l10n.nextTooltip, + ), + ] + ], + ), + ), + MultiCrossFader( + duration: Durations.xmpStructArrayCardTransition, + sizeCurve: Curves.easeOutBack, + alignment: AlignmentDirectional.topStart, + child: Padding( + // add padding at this level (instead of the column level) + // so that the crossfader can animate the content size + // without clipping the text + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), + child: InfoRowGroup( + info: Map.fromEntries(props.map((prop) => MapEntry(prop.displayKey, widget.formatValue(prop)))), + maxValueLength: Constants.infoGroupMaxValueLength, + spanBuilders: widget.spanBuilders?.call(_isIndexed ? index + 1 : null), + ), + ), + ), + if (cards != null) + ...cards.where((v) => !v.isEmpty).map((card) { + final spanBuilders = card.spanBuilders; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: XmpCard( + title: card.title, + structByIndex: card.data, + formatValue: widget.formatValue, + spanBuilders: spanBuilders != null ? (index) => spanBuilders(index, card.data[index]!.item1) : null, + ), + ); + }), + ], + ); + }, + ), + ); + } + + void _setIndex(int index) => _indexNotifier.value = index.clamp(0, indexedStructCount - 1); +} diff --git a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart index b912b90e0..937bfda3f 100644 --- a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart +++ b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart @@ -5,17 +5,12 @@ import 'package:aves/utils/string_utils.dart'; import 'package:aves/utils/xmp_utils.dart'; import 'package:aves/widgets/common/identity/highlight_title.dart'; import 'package:aves/widgets/viewer/info/common.dart'; +import 'package:aves/widgets/viewer/info/metadata/xmp_card.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/crs.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_ns/darktable.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_ns/dwc.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/exif.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/google.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_ns/iptc.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_ns/iptc4xmpext.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_ns/microsoft.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_ns/mwg.dart'; +import 'package:aves/widgets/viewer/info/metadata/xmp_ns/misc.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/photoshop.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_ns/plus.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/tiff.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/xmp.dart'; import 'package:collection/collection.dart'; @@ -23,6 +18,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; @immutable class XmpNamespace extends Equatable { @@ -38,6 +34,8 @@ class XmpNamespace extends Equatable { switch (nsUri) { case Namespaces.container: return XmpContainer(nsPrefix, rawProps); + case Namespaces.creatorAtom: + return XmpCreatorAtom(nsPrefix, rawProps); case Namespaces.crs: return XmpCrsNamespace(nsPrefix, rawProps); case Namespaces.darktable: @@ -70,8 +68,6 @@ class XmpNamespace extends Equatable { return XmpBasicNamespace(nsPrefix, rawProps); case Namespaces.xmpMM: return XmpMMNamespace(nsPrefix, rawProps); - case Namespaces.xmpNote: - return XmpNoteNamespace(nsPrefix, rawProps); default: return XmpNamespace(nsUri, nsPrefix, rawProps); } @@ -79,26 +75,37 @@ class XmpNamespace extends Equatable { String get displayTitle => Namespaces.nsTitles[nsUri] ?? (nsPrefix.isEmpty ? nsUri : '${nsPrefix.substring(0, nsPrefix.length - 1)} ($nsUri)'); - Map get buildProps => rawProps; - List buildNamespaceSection(BuildContext context) { - final props = buildProps.entries + final props = rawProps.entries .map((kv) { final prop = XmpProp(kv.key, kv.value); - return extractData(prop) ? null : prop; + var extracted = false; + cards.forEach((card) => extracted |= card.extract(prop)); + return extracted ? null : prop; }) .whereNotNull() .toList() - ..sort((a, b) => compareAsciiUpperCaseNatural(a.displayKey, b.displayKey)); + ..sort(); final content = [ if (props.isNotEmpty) InfoRowGroup( - info: Map.fromEntries(props.map((prop) => MapEntry(prop.displayKey, formatValue(prop)))), + info: Map.fromEntries(props.map((v) => MapEntry(v.displayKey, formatValue(v)))), maxValueLength: Constants.infoGroupMaxValueLength, spanBuilders: linkifyValues(props), ), - ...buildFromExtractedData(), + ...cards.where((v) => !v.isEmpty).map((card) { + final spanBuilders = card.spanBuilders; + return Padding( + padding: const EdgeInsets.only(top: 8), + child: XmpCard( + title: card.title, + structByIndex: card.data, + formatValue: formatValue, + spanBuilders: spanBuilders != null ? (index) => spanBuilders(index, card.data[index]!.item1) : null, + ), + ); + }), ]; return content.isNotEmpty @@ -117,38 +124,14 @@ class XmpNamespace extends Equatable { : []; } - bool extractStruct(XmpProp prop, RegExp pattern, Map store) { - final matches = pattern.allMatches(prop.path); - if (matches.isEmpty) return false; - - final match = matches.first; - final field = XmpProp.formatKey(match.group(1)!); - store[field] = formatValue(prop); - return true; - } - - bool extractIndexedStruct(XmpProp prop, RegExp pattern, Map> store) { - final matches = pattern.allMatches(prop.path); - if (matches.isEmpty) return false; - - final match = matches.first; - final index = int.parse(match.group(1)!); - final field = XmpProp.formatKey(match.group(2)!); - final fields = store.putIfAbsent(index, () => {}); - fields[field] = formatValue(prop); - return true; - } - - bool extractData(XmpProp prop) => false; - - List buildFromExtractedData() => []; + List get cards => []; String formatValue(XmpProp prop) => prop.value; Map linkifyValues(List props) => {}; } -class XmpProp { +class XmpProp implements Comparable { final String path, value; final String displayKey; @@ -165,6 +148,82 @@ class XmpProp { }); } + @override + int compareTo(XmpProp other) => compareAsciiUpperCaseNatural(displayKey, other.displayKey); + @override String toString() => '$runtimeType#${shortHash(this)}{path=$path, value=$value}'; } + +class XmpCardData { + final String title; + final RegExp pattern; + final bool indexed; + final Map Function(int?, Map data)? spanBuilders; + final List? cards; + final Map data = {}; + + bool get isEmpty => data.isEmpty && (cards?.every((card) => card.isEmpty) ?? true); + + static final titlePattern = RegExp(r'(.*?)[\\/]'); + + XmpCardData( + this.pattern, { + String? title, + this.spanBuilders, + this.cards, + }) : indexed = pattern.pattern.contains(r'\[(\d+)\]'), + title = title ?? XmpProp.formatKey(titlePattern.firstMatch(pattern.pattern)!.group(1)!); + + XmpCardData cloneEmpty() { + return XmpCardData( + pattern, + title: title, + spanBuilders: spanBuilders, + cards: cards?.map((v) => v.cloneEmpty()).toList(), + ); + } + + bool extract(XmpProp prop) => indexed ? _extractIndexedStruct(prop) : _extractDirectStruct(prop); + + bool _extractDirectStruct(XmpProp prop) { + final matches = pattern.allMatches(prop.path); + if (matches.isEmpty) return false; + + final match = matches.first; + final field = match.group(1)!; + + final fields = data.putIfAbsent(null, () => Tuple2({}, cards?.map((v) => v.cloneEmpty()).toList())); + final _cards = fields.item2; + if (_cards != null) { + final fieldProp = XmpProp(field, prop.value); + if (_cards.any((v) => v.extract(fieldProp))) { + return true; + } + } + + fields.item1[field] = prop; + return true; + } + + bool _extractIndexedStruct(XmpProp prop) { + final matches = pattern.allMatches(prop.path); + if (matches.isEmpty) return false; + + final match = matches.first; + final index = int.parse(match.group(1)!); + final field = match.group(2)!; + + final fields = data.putIfAbsent(index, () => Tuple2({}, cards?.map((v) => v.cloneEmpty()).toList())); + final _cards = fields.item2; + if (_cards != null) { + final fieldProp = XmpProp(field, prop.value); + if (_cards.any((v) => v.extract(fieldProp))) { + return true; + } + } + + fields.item1[field] = prop; + return true; + } +} diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/crs.dart b/lib/widgets/viewer/info/metadata/xmp_ns/crs.dart index 40933671f..adc24490e 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/crs.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/crs.dart @@ -1,75 +1,50 @@ import 'package:aves/utils/xmp_utils.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart'; -import 'package:flutter/widgets.dart'; class XmpCrsNamespace extends XmpNamespace { - late final cgbcPattern = RegExp(nsPrefix + r'CircularGradientBasedCorrections\[(\d+)\]/(.*)'); - late final gbcPattern = RegExp(nsPrefix + r'GradientBasedCorrections\[(\d+)\]/(.*)'); - late final mgbcPattern = RegExp(nsPrefix + r'MaskGroupBasedCorrections\[(\d+)\]/(.*)'); - late final pbcPattern = RegExp(nsPrefix + r'PaintBasedCorrections\[(\d+)\]/(.*)'); - late final retouchAreasPattern = RegExp(nsPrefix + r'RetouchAreas\[(\d+)\]/(.*)'); - late final lookPattern = RegExp(nsPrefix + r'Look/(.*)'); - late final rmmiPattern = RegExp(nsPrefix + r'RangeMaskMapInfo/' + nsPrefix + r'RangeMaskMapInfo/(.*)'); - - final cgbc = >{}; - final gbc = >{}; - final mgbc = >{}; - final pbc = >{}; - final retouchAreas = >{}; - final look = {}; - final rmmi = {}; - XmpCrsNamespace(String nsPrefix, Map rawProps) : super(Namespaces.crs, nsPrefix, rawProps); @override - bool extractData(XmpProp prop) { - var hasStructs = extractStruct(prop, lookPattern, look); - hasStructs |= extractStruct(prop, rmmiPattern, rmmi); - var hasIndexedStructs = extractIndexedStruct(prop, cgbcPattern, cgbc); - hasIndexedStructs |= extractIndexedStruct(prop, gbcPattern, gbc); - hasIndexedStructs |= extractIndexedStruct(prop, mgbcPattern, mgbc); - hasIndexedStructs |= extractIndexedStruct(prop, pbcPattern, pbc); - hasIndexedStructs |= extractIndexedStruct(prop, retouchAreasPattern, retouchAreas); - return hasStructs || hasIndexedStructs; - } - - @override - List buildFromExtractedData() => [ - if (cgbc.isNotEmpty) - XmpStructArrayCard( - title: 'Circular Gradient Based Corrections', - structByIndex: cgbc, - ), - if (gbc.isNotEmpty) - XmpStructArrayCard( - title: 'Gradient Based Corrections', - structByIndex: gbc, - ), - if (look.isNotEmpty) - XmpStructCard( - title: 'Look', - struct: look, - ), - if (mgbc.isNotEmpty) - XmpStructArrayCard( - title: 'Mask Group Based Corrections', - structByIndex: mgbc, - ), - if (pbc.isNotEmpty) - XmpStructArrayCard( - title: 'Paint Based Corrections', - structByIndex: pbc, - ), - if (rmmi.isNotEmpty) - XmpStructCard( - title: 'Range Mask Map Info', - struct: rmmi, - ), - if (retouchAreas.isNotEmpty) - XmpStructArrayCard( - title: 'Retouch Areas', - structByIndex: retouchAreas, - ), - ]; + late final List cards = [ + XmpCardData(RegExp(nsPrefix + r'CircularGradientBasedCorrections\[(\d+)\]/(.*)')), + XmpCardData( + RegExp(nsPrefix + r'GradientBasedCorrections\[(\d+)\]/(.*)'), + cards: [ + XmpCardData(RegExp(nsPrefix + r'CorrectionMasks\[(\d+)\]/(.*)')), + XmpCardData(RegExp(nsPrefix + r'CorrectionRangeMask/(.*)')), + ], + ), + XmpCardData( + RegExp(nsPrefix + r'Look/(.*)'), + cards: [ + XmpCardData(RegExp(nsPrefix + r'Parameters/(.*)')), + ], + ), + XmpCardData( + RegExp(nsPrefix + r'MaskGroupBasedCorrections\[(\d+)\]/(.*)'), + cards: [ + XmpCardData( + RegExp(nsPrefix + r'CorrectionMasks\[(\d+)\]/(.*)'), + cards: [ + XmpCardData(RegExp(nsPrefix + r'CorrectionRangeMask/(.*)')), + XmpCardData(RegExp(nsPrefix + r'Masks\[(\d+)\]/(.*)')), + ], + ), + ], + ), + XmpCardData( + RegExp(nsPrefix + r'PaintBasedCorrections\[(\d+)\]/(.*)'), + cards: [ + XmpCardData(RegExp(nsPrefix + r'CorrectionMasks\[(\d+)\]/(.*)')), + XmpCardData(RegExp(nsPrefix + r'CorrectionRangeMask/(.*)')), + ], + ), + XmpCardData( + RegExp(nsPrefix + r'RetouchAreas\[(\d+)\]/(.*)'), + cards: [ + XmpCardData(RegExp(nsPrefix + r'Masks\[(\d+)\]/(.*)')), + ], + ), + XmpCardData(RegExp(nsPrefix + r'RangeMaskMapInfo/' + nsPrefix + r'RangeMaskMapInfo/(.*)')), + ]; } diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/darktable.dart b/lib/widgets/viewer/info/metadata/xmp_ns/darktable.dart deleted file mode 100644 index e47f67b4e..000000000 --- a/lib/widgets/viewer/info/metadata/xmp_ns/darktable.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:aves/utils/xmp_utils.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart'; -import 'package:flutter/material.dart'; - -class XmpDarktableNamespace extends XmpNamespace { - late final historyPattern = RegExp(nsPrefix + r'history\[(\d+)\]/(.*)'); - - final history = >{}; - - XmpDarktableNamespace(String nsPrefix, Map rawProps) : super(Namespaces.darktable, nsPrefix, rawProps); - - @override - bool extractData(XmpProp prop) => extractIndexedStruct(prop, historyPattern, history); - - @override - List buildFromExtractedData() => [ - if (history.isNotEmpty) - XmpStructArrayCard( - title: 'History', - structByIndex: history, - ), - ]; -} diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/dwc.dart b/lib/widgets/viewer/info/metadata/xmp_ns/dwc.dart deleted file mode 100644 index ad5fe4212..000000000 --- a/lib/widgets/viewer/info/metadata/xmp_ns/dwc.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:aves/utils/xmp_utils.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart'; -import 'package:flutter/widgets.dart'; - -class XmpDwcNamespace extends XmpNamespace { - late final dcTermsLocationPattern = RegExp(nsPrefix + r'dctermsLocation/(.*)'); - late final eventPattern = RegExp(nsPrefix + r'Event/(.*)'); - late final geologicalContextPattern = RegExp(nsPrefix + r'GeologicalContext/(.*)'); - late final identificationPattern = RegExp(nsPrefix + r'Identification/(.*)'); - late final measurementOrFactPattern = RegExp(nsPrefix + r'MeasurementOrFact/(.*)'); - late final occurrencePattern = RegExp(nsPrefix + r'Occurrence/(.*)'); - late final recordPattern = RegExp(nsPrefix + r'Record/(.*)'); - late final resourceRelationshipPattern = RegExp(nsPrefix + r'ResourceRelationship/(.*)'); - late final taxonPattern = RegExp(nsPrefix + r'Taxon/(.*)'); - - final dcTermsLocation = {}; - final event = {}; - final identification = {}; - final geologicalContext = {}; - final measurementOrFact = {}; - final occurrence = {}; - final record = {}; - final resourceRelationship = {}; - final taxon = {}; - - XmpDwcNamespace(String nsPrefix, Map rawProps) : super(Namespaces.dwc, nsPrefix, rawProps); - - @override - bool extractData(XmpProp prop) { - var hasStructs = extractStruct(prop, dcTermsLocationPattern, dcTermsLocation); - hasStructs |= extractStruct(prop, eventPattern, event); - hasStructs |= extractStruct(prop, geologicalContextPattern, geologicalContext); - hasStructs |= extractStruct(prop, measurementOrFactPattern, measurementOrFact); - hasStructs |= extractStruct(prop, identificationPattern, identification); - hasStructs |= extractStruct(prop, occurrencePattern, occurrence); - hasStructs |= extractStruct(prop, recordPattern, record); - hasStructs |= extractStruct(prop, resourceRelationshipPattern, resourceRelationship); - hasStructs |= extractStruct(prop, taxonPattern, taxon); - return hasStructs; - } - - @override - List buildFromExtractedData() => [ - if (dcTermsLocation.isNotEmpty) - XmpStructCard( - title: 'DC Terms Location', - struct: dcTermsLocation, - ), - if (event.isNotEmpty) - XmpStructCard( - title: 'Event', - struct: event, - ), - if (geologicalContext.isNotEmpty) - XmpStructCard( - title: 'Geological Context', - struct: geologicalContext, - ), - if (identification.isNotEmpty) - XmpStructCard( - title: 'Identification', - struct: identification, - ), - if (measurementOrFact.isNotEmpty) - XmpStructCard( - title: 'Measurement Or Fact', - struct: measurementOrFact, - ), - if (occurrence.isNotEmpty) - XmpStructCard( - title: 'Occurrence', - struct: occurrence, - ), - if (record.isNotEmpty) - XmpStructCard( - title: 'Record', - struct: record, - ), - if (resourceRelationship.isNotEmpty) - XmpStructCard( - title: 'Resource Relationship', - struct: resourceRelationship, - ), - if (taxon.isNotEmpty) - XmpStructCard( - title: 'Taxon', - struct: taxon, - ), - ]; -} diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart index 9a3270386..419d43fd4 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart @@ -3,9 +3,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/embedded/notifications.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/widgets.dart'; import 'package:tuple/tuple.dart'; abstract class XmpGoogleNamespace extends XmpNamespace { @@ -79,21 +77,10 @@ class XmpGImageNamespace extends XmpGoogleNamespace { } class XmpContainer extends XmpNamespace { - late final directoryPattern = RegExp('${nsPrefix}Directory\\[(\\d+)\\]/${nsPrefix}Item/(.*)'); - - final directories = >{}; - XmpContainer(String nsPrefix, Map rawProps) : super(Namespaces.container, nsPrefix, rawProps); @override - bool extractData(XmpProp prop) => extractIndexedStruct(prop, directoryPattern, directories); - - @override - List buildFromExtractedData() => [ - if (directories.isNotEmpty) - XmpStructArrayCard( - title: 'Directory Item', - structByIndex: directories, - ), - ]; + late final List cards = [ + XmpCardData(RegExp('${nsPrefix}Directory\\[(\\d+)\\]/${nsPrefix}Item/(.*)'), title: 'Directory Item'), + ]; } diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/iptc.dart b/lib/widgets/viewer/info/metadata/xmp_ns/iptc.dart deleted file mode 100644 index 167069b2a..000000000 --- a/lib/widgets/viewer/info/metadata/xmp_ns/iptc.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:aves/utils/xmp_utils.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart'; -import 'package:flutter/material.dart'; - -class XmpIptcCoreNamespace extends XmpNamespace { - late final creatorContactInfoPattern = RegExp(nsPrefix + r'CreatorContactInfo/(.*)'); - - final creatorContactInfo = {}; - - XmpIptcCoreNamespace(String nsPrefix, Map rawProps) : super(Namespaces.iptc4xmpCore, nsPrefix, rawProps); - - @override - bool extractData(XmpProp prop) => extractStruct(prop, creatorContactInfoPattern, creatorContactInfo); - - @override - List buildFromExtractedData() => [ - if (creatorContactInfo.isNotEmpty) - XmpStructCard( - title: 'Creator Contact Info', - struct: creatorContactInfo, - ), - ]; -} diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/iptc4xmpext.dart b/lib/widgets/viewer/info/metadata/xmp_ns/iptc4xmpext.dart deleted file mode 100644 index 9d72e8952..000000000 --- a/lib/widgets/viewer/info/metadata/xmp_ns/iptc4xmpext.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:aves/utils/xmp_utils.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart'; -import 'package:flutter/material.dart'; - -class XmpIptc4xmpExtNamespace extends XmpNamespace { - late final aooPattern = RegExp(nsPrefix + r'ArtworkOrObject\[(\d+)\]/(.*)'); - - final aoo = >{}; - - XmpIptc4xmpExtNamespace(String nsPrefix, Map rawProps) : super(Namespaces.iptc4xmpExt, nsPrefix, rawProps); - - @override - bool extractData(XmpProp prop) => extractIndexedStruct(prop, aooPattern, aoo); - - @override - List buildFromExtractedData() => [ - if (aoo.isNotEmpty) - XmpStructArrayCard( - title: 'Artwork or Object', - structByIndex: aoo, - ), - ]; -} diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/microsoft.dart b/lib/widgets/viewer/info/metadata/xmp_ns/microsoft.dart deleted file mode 100644 index af5d1b9cc..000000000 --- a/lib/widgets/viewer/info/metadata/xmp_ns/microsoft.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:aves/utils/xmp_utils.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart'; -import 'package:flutter/widgets.dart'; - -class XmpMPNamespace extends XmpNamespace { - late final regionListPattern = RegExp(nsPrefix + r'RegionInfo/MPRI:Regions\[(\d+)\]/(.*)'); - - final regionList = >{}; - - XmpMPNamespace(String nsPrefix, Map rawProps) : super(Namespaces.mp, nsPrefix, rawProps); - - @override - bool extractData(XmpProp prop) => extractIndexedStruct(prop, regionListPattern, regionList); - - @override - List buildFromExtractedData() => [ - if (regionList.isNotEmpty) - XmpStructArrayCard( - title: 'Regions', - structByIndex: regionList, - ), - ]; -} diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/misc.dart b/lib/widgets/viewer/info/metadata/xmp_ns/misc.dart new file mode 100644 index 000000000..90b07db8f --- /dev/null +++ b/lib/widgets/viewer/info/metadata/xmp_ns/misc.dart @@ -0,0 +1,96 @@ +import 'package:aves/utils/xmp_utils.dart'; +import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; + +class XmpCreatorAtom extends XmpNamespace { + XmpCreatorAtom(String nsPrefix, Map rawProps) : super(Namespaces.creatorAtom, nsPrefix, rawProps); + + @override + late final List cards = [ + XmpCardData(RegExp(nsPrefix + r'aeProjectLink/(.*)'), title: 'AE Project Link'), + ]; +} + +class XmpDarktableNamespace extends XmpNamespace { + XmpDarktableNamespace(String nsPrefix, Map rawProps) : super(Namespaces.darktable, nsPrefix, rawProps); + + @override + late final List cards = [ + XmpCardData(RegExp(nsPrefix + r'history\[(\d+)\]/(.*)')), + ]; +} + +class XmpDwcNamespace extends XmpNamespace { + XmpDwcNamespace(String nsPrefix, Map rawProps) : super(Namespaces.dwc, nsPrefix, rawProps); + + @override + late final List cards = [ + XmpCardData(RegExp(nsPrefix + r'dctermsLocation/(.*)'), title: 'DC Terms Location'), + XmpCardData(RegExp(nsPrefix + r'Event/(.*)')), + XmpCardData(RegExp(nsPrefix + r'GeologicalContext/(.*)')), + XmpCardData(RegExp(nsPrefix + r'Identification/(.*)')), + XmpCardData(RegExp(nsPrefix + r'MeasurementOrFact/(.*)')), + XmpCardData(RegExp(nsPrefix + r'Occurrence/(.*)')), + XmpCardData(RegExp(nsPrefix + r'Record/(.*)')), + XmpCardData(RegExp(nsPrefix + r'ResourceRelationship/(.*)')), + XmpCardData(RegExp(nsPrefix + r'Taxon/(.*)')), + ]; +} + +class XmpIptcCoreNamespace extends XmpNamespace { + XmpIptcCoreNamespace(String nsPrefix, Map rawProps) : super(Namespaces.iptc4xmpCore, nsPrefix, rawProps); + + @override + late final List cards = [ + XmpCardData(RegExp(nsPrefix + r'CreatorContactInfo/(.*)')), + ]; +} + +class XmpIptc4xmpExtNamespace extends XmpNamespace { + XmpIptc4xmpExtNamespace(String nsPrefix, Map rawProps) : super(Namespaces.iptc4xmpExt, nsPrefix, rawProps); + + @override + late final List cards = [ + XmpCardData(RegExp(nsPrefix + r'ArtworkOrObject\[(\d+)\]/(.*)')), + ]; +} + +class XmpMPNamespace extends XmpNamespace { + XmpMPNamespace(String nsPrefix, Map rawProps) : super(Namespaces.mp, nsPrefix, rawProps); + + @override + late final List cards = [ + XmpCardData(RegExp(nsPrefix + r'RegionInfo/MPRI:Regions\[(\d+)\]/(.*)'), title: 'Regions'), + ]; +} + +// cf www.metadataworkinggroup.org/pdf/mwg_guidance.pdf (down, as of 2021/02/15) +class XmpMgwRegionsNamespace extends XmpNamespace { + XmpMgwRegionsNamespace(String nsPrefix, Map rawProps) : super(Namespaces.mwgrs, nsPrefix, rawProps); + + @override + late final List cards = [ + XmpCardData(RegExp(nsPrefix + r'Regions/mwg-rs:AppliedToDimensions/(.*)'), title: 'Applied to Dimensions'), + XmpCardData(RegExp(nsPrefix + r'Regions/mwg-rs:RegionList\[(\d+)\]/(.*)'), title: 'Region List'), + ]; +} + +class XmpPlusNamespace extends XmpNamespace { + XmpPlusNamespace(String nsPrefix, Map rawProps) : super(Namespaces.plus, nsPrefix, rawProps); + + @override + late final List cards = [ + XmpCardData(RegExp(nsPrefix + r'Licensor\[(\d+)\]/(.*)')), + ]; +} + +class XmpMMNamespace extends XmpNamespace { + XmpMMNamespace(String nsPrefix, Map rawProps) : super(Namespaces.xmpMM, nsPrefix, rawProps); + + @override + late final List cards = [ + XmpCardData(RegExp(nsPrefix + r'DerivedFrom/(.*)')), + XmpCardData(RegExp(nsPrefix + r'History\[(\d+)\]/(.*)')), + XmpCardData(RegExp(nsPrefix + r'Ingredients\[(\d+)\]/(.*)')), + XmpCardData(RegExp(nsPrefix + r'Pantry\[(\d+)\]/(.*)')), + ]; +} diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/mwg.dart b/lib/widgets/viewer/info/metadata/xmp_ns/mwg.dart deleted file mode 100644 index 3c74449f6..000000000 --- a/lib/widgets/viewer/info/metadata/xmp_ns/mwg.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:aves/utils/xmp_utils.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart'; -import 'package:flutter/widgets.dart'; - -// cf www.metadataworkinggroup.org/pdf/mwg_guidance.pdf (down, as of 2021/02/15) -class XmpMgwRegionsNamespace extends XmpNamespace { - late final dimensionsPattern = RegExp(nsPrefix + r'Regions/mwg-rs:AppliedToDimensions/(.*)'); - late final regionListPattern = RegExp(nsPrefix + r'Regions/mwg-rs:RegionList\[(\d+)\]/(.*)'); - - final dimensions = {}; - final regionList = >{}; - - XmpMgwRegionsNamespace(String nsPrefix, Map rawProps) : super(Namespaces.mwgrs, nsPrefix, rawProps); - - @override - bool extractData(XmpProp prop) { - final hasStructs = extractStruct(prop, dimensionsPattern, dimensions); - final hasIndexedStructs = extractIndexedStruct(prop, regionListPattern, regionList); - return hasStructs || hasIndexedStructs; - } - - @override - List buildFromExtractedData() => [ - if (dimensions.isNotEmpty) - XmpStructCard( - title: 'Applied To Dimensions', - struct: dimensions, - ), - if (regionList.isNotEmpty) - XmpStructArrayCard( - title: 'Region', - structByIndex: regionList, - ), - ]; -} diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart b/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart index 992d96306..e6a749ec7 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/photoshop.dart @@ -1,29 +1,20 @@ import 'package:aves/utils/xmp_utils.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart'; -import 'package:flutter/widgets.dart'; // cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/photoshop.md class XmpPhotoshopNamespace extends XmpNamespace { - late final textLayersPattern = RegExp(nsPrefix + r'TextLayers\[(\d+)\]/(.*)'); - - final textLayers = >{}; - XmpPhotoshopNamespace(String nsPrefix, Map rawProps) : super(Namespaces.photoshop, nsPrefix, rawProps); @override - bool extractData(XmpProp prop) { - return extractIndexedStruct(prop, textLayersPattern, textLayers); - } - - @override - List buildFromExtractedData() => [ - if (textLayers.isNotEmpty) - XmpStructArrayCard( - title: 'Text Layers', - structByIndex: textLayers, - ), - ]; + late final List cards = [ + XmpCardData( + RegExp(nsPrefix + r'CameraProfiles\[(\d+)\]/(.*)'), + cards: [ + XmpCardData(RegExp(r'crlcp:PerspectiveModel/(.*)')), + ], + ), + XmpCardData(RegExp(nsPrefix + r'TextLayers\[(\d+)\]/(.*)')), + ]; @override String formatValue(XmpProp prop) { diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/plus.dart b/lib/widgets/viewer/info/metadata/xmp_ns/plus.dart deleted file mode 100644 index 34ab0862e..000000000 --- a/lib/widgets/viewer/info/metadata/xmp_ns/plus.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:aves/utils/xmp_utils.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart'; -import 'package:flutter/material.dart'; - -class XmpPlusNamespace extends XmpNamespace { - late final licensorPattern = RegExp(nsPrefix + r'Licensor\[(\d+)\]/(.*)'); - - final licensor = >{}; - - XmpPlusNamespace(String nsPrefix, Map rawProps) : super(Namespaces.plus, nsPrefix, rawProps); - - @override - bool extractData(XmpProp prop) => extractIndexedStruct(prop, licensorPattern, licensor); - - @override - List buildFromExtractedData() => [ - if (licensor.isNotEmpty) - XmpStructArrayCard( - title: 'Licensor', - structByIndex: licensor, - ), - ]; -} diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart index 042037017..fceb37783 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart @@ -4,102 +4,30 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/embedded/notifications.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; -import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart'; -import 'package:flutter/material.dart'; class XmpBasicNamespace extends XmpNamespace { - late final thumbnailsPattern = RegExp(nsPrefix + r'Thumbnails\[(\d+)\]/(.*)'); - static const thumbnailDataDisplayKey = 'Image'; - - final thumbnails = >{}; - XmpBasicNamespace(String nsPrefix, Map rawProps) : super(Namespaces.xmp, nsPrefix, rawProps); @override - bool extractData(XmpProp prop) => extractIndexedStruct(prop, thumbnailsPattern, thumbnails); - - @override - List buildFromExtractedData() => [ - if (thumbnails.isNotEmpty) - XmpStructArrayCard( - title: 'Thumbnail', - structByIndex: thumbnails, - linkifier: (index) { - final struct = thumbnails[index]!; - return { - if (struct.containsKey(thumbnailDataDisplayKey)) - thumbnailDataDisplayKey: InfoRowGroup.linkSpanBuilder( - linkText: (context) => context.l10n.viewerInfoOpenLinkText, - onTap: (context) => OpenEmbeddedDataNotification.xmp( - props: [ - const [Namespaces.xmp, 'Thumbnails'], - index, - const [Namespaces.xmpGImg, 'image'], - ], - mimeType: MimeTypes.jpeg, - ).dispatch(context), - ), - }; - }, - ) - ]; -} - -class XmpMMNamespace extends XmpNamespace { - late final derivedFromPattern = RegExp(nsPrefix + r'DerivedFrom/(.*)'); - late final historyPattern = RegExp(nsPrefix + r'History\[(\d+)\]/(.*)'); - late final ingredientsPattern = RegExp(nsPrefix + r'Ingredients\[(\d+)\]/(.*)'); - late final pantryPattern = RegExp(nsPrefix + r'Pantry\[(\d+)\]/(.*)'); - - final derivedFrom = {}; - final history = >{}; - final ingredients = >{}; - final pantry = >{}; - - XmpMMNamespace(String nsPrefix, Map rawProps) : super(Namespaces.xmpMM, nsPrefix, rawProps); - - @override - bool extractData(XmpProp prop) { - final hasStructs = extractStruct(prop, derivedFromPattern, derivedFrom); - var hasIndexedStructs = extractIndexedStruct(prop, historyPattern, history); - hasIndexedStructs |= extractIndexedStruct(prop, ingredientsPattern, ingredients); - hasIndexedStructs |= extractIndexedStruct(prop, pantryPattern, pantry); - return hasStructs || hasIndexedStructs; - } - - @override - List buildFromExtractedData() => [ - if (derivedFrom.isNotEmpty) - XmpStructCard( - title: 'Derived From', - struct: derivedFrom, - ), - if (history.isNotEmpty) - XmpStructArrayCard( - title: 'History', - structByIndex: history, - ), - if (ingredients.isNotEmpty) - XmpStructArrayCard( - title: 'Ingredients', - structByIndex: ingredients, - ), - if (pantry.isNotEmpty) - XmpStructArrayCard( - title: 'Pantry', - structByIndex: pantry, - ), - ]; -} - -class XmpNoteNamespace extends XmpNamespace { - // `xmpNote:HasExtendedXMP` is structural and should not be displayed to users - late final hasExtendedXmp = '${nsPrefix}HasExtendedXMP'; - - XmpNoteNamespace(String nsPrefix, Map rawProps) : super(Namespaces.xmpNote, nsPrefix, rawProps); - - @override - bool extractData(XmpProp prop) { - return prop.path == hasExtendedXmp; - } + late final List cards = [ + XmpCardData( + RegExp(nsPrefix + r'Thumbnails\[(\d+)\]/(.*)'), + spanBuilders: (index, struct) { + return { + if (struct.containsKey('xmpGImg:image')) + 'Image': InfoRowGroup.linkSpanBuilder( + linkText: (context) => context.l10n.viewerInfoOpenLinkText, + onTap: (context) => OpenEmbeddedDataNotification.xmp( + props: [ + const [Namespaces.xmp, 'Thumbnails'], + index, + const [Namespaces.xmpGImg, 'image'], + ], + mimeType: MimeTypes.jpeg, + ).dispatch(context), + ), + }; + }, + ), + ]; } diff --git a/lib/widgets/viewer/info/metadata/xmp_structs.dart b/lib/widgets/viewer/info/metadata/xmp_structs.dart deleted file mode 100644 index 88f66e790..000000000 --- a/lib/widgets/viewer/info/metadata/xmp_structs.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'dart:math'; - -import 'package:aves/theme/durations.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:aves/theme/themes.dart'; -import 'package:aves/utils/constants.dart'; -import 'package:aves/widgets/common/basic/multi_cross_fader.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/identity/highlight_title.dart'; -import 'package:aves/widgets/viewer/info/common.dart'; -import 'package:flutter/material.dart'; - -class XmpStructArrayCard extends StatefulWidget { - final String title; - late final List> structs; - final Map Function(int index)? linkifier; - - XmpStructArrayCard({ - super.key, - required this.title, - required Map> structByIndex, - this.linkifier, - }) { - final length = structByIndex.keys.fold(0, max); - structs = [for (var i = 0; i < length; i++) structByIndex[i + 1] ?? {}]; - } - - @override - State createState() => _XmpStructArrayCardState(); -} - -class _XmpStructArrayCardState extends State { - late int _index; - - List> get structs => widget.structs; - - @override - void initState() { - super.initState(); - _index = structs.length - 1; - } - - @override - Widget build(BuildContext context) { - void setIndex(int index) { - index = index.clamp(0, structs.length - 1); - if (_index != index) { - _index = index; - setState(() {}); - } - } - - return Card( - color: Themes.thirdLayerColor(context), - margin: XmpStructCard.cardMargin, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 8, top: 8, right: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: HighlightTitle( - title: '${widget.title} ${_index + 1}', - selectable: true, - showHighlight: false, - ), - ), - IconButton( - visualDensity: VisualDensity.compact, - icon: const Icon(AIcons.previous), - onPressed: _index > 0 ? () => setIndex(_index - 1) : null, - tooltip: context.l10n.previousTooltip, - ), - IconButton( - visualDensity: VisualDensity.compact, - icon: const Icon(AIcons.next), - onPressed: _index < structs.length - 1 ? () => setIndex(_index + 1) : null, - tooltip: context.l10n.nextTooltip, - ), - ], - ), - ), - MultiCrossFader( - duration: Durations.xmpStructArrayCardTransition, - sizeCurve: Curves.easeOutBack, - alignment: AlignmentDirectional.topStart, - child: Padding( - // add padding at this level (instead of the column level) - // so that the crossfader can animate the content size - // without clipping the text - padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: InfoRowGroup( - info: structs[_index], - maxValueLength: Constants.infoGroupMaxValueLength, - spanBuilders: widget.linkifier?.call(_index + 1), - ), - ), - ), - ], - ), - ); - } -} - -class XmpStructCard extends StatelessWidget { - final String title; - final Map struct; - final Map Function()? linkifier; - - static const cardMargin = EdgeInsets.symmetric(vertical: 8, horizontal: 0); - - const XmpStructCard({ - super.key, - required this.title, - required this.struct, - this.linkifier, - }); - - @override - Widget build(BuildContext context) { - return Card( - color: Themes.thirdLayerColor(context), - margin: cardMargin, - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - HighlightTitle( - title: title, - selectable: true, - showHighlight: false, - ), - InfoRowGroup( - info: struct, - maxValueLength: Constants.infoGroupMaxValueLength, - spanBuilders: linkifier?.call(), - ), - ], - ), - ), - ); - } -} diff --git a/lib/widgets/viewer/overlay/details.dart b/lib/widgets/viewer/overlay/details.dart index 53a8adc74..fb4e69f8c 100644 --- a/lib/widgets/viewer/overlay/details.dart +++ b/lib/widgets/viewer/overlay/details.dart @@ -145,7 +145,7 @@ class ViewerDetailOverlayContent extends StatelessWidget { builder: (context, child) { final positionTitle = _PositionTitleRow(entry: pageEntry, collectionPosition: position, multiPageController: multiPageController); return DefaultTextStyle( - style: Theme.of(context).textTheme.bodyText2!.copyWith( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( shadows: _shadows(context), ), softWrap: false, diff --git a/lib/widgets/viewer/overlay/wallpaper_buttons.dart b/lib/widgets/viewer/overlay/wallpaper_buttons.dart index 1e9e0022d..39dd5b9eb 100644 --- a/lib/widgets/viewer/overlay/wallpaper_buttons.dart +++ b/lib/widgets/viewer/overlay/wallpaper_buttons.dart @@ -2,14 +2,13 @@ import 'dart:async'; import 'dart:math'; import 'dart:ui' as ui; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; import 'package:aves/model/wallpaper_target.dart'; import 'package:aves/services/wallpaper_service.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves/widgets/dialogs/wallpaper_settings_dialog.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; @@ -18,6 +17,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; class WallpaperButtons extends StatelessWidget with FeedbackMixin { final AvesEntry entry; @@ -56,19 +56,14 @@ class WallpaperButtons extends StatelessWidget with FeedbackMixin { Future _setWallpaper(BuildContext context) async { final l10n = context.l10n; - var target = WallpaperTarget.home; - if (device.canSetLockScreenWallpaper) { - final value = await showDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: WallpaperTarget.home, - options: Map.fromEntries(WallpaperTarget.values.map((v) => MapEntry(v, v.getName(context)))), - confirmationButtonLabel: l10n.continueButtonLabel, - ), - ); - if (value == null) return; - target = value; - } + final value = await showDialog>( + context: context, + builder: (context) => const WallpaperSettingsDialog(), + ); + if (value == null) return; + + final target = value.item1; + final useScrollEffect = value.item2; final reportController = StreamController.broadcast(); unawaited(showOpReport( @@ -76,17 +71,15 @@ class WallpaperButtons extends StatelessWidget with FeedbackMixin { opStream: reportController.stream, )); - final viewState = context.read().getOrCreateController(entry).value; - final viewportSize = viewState.viewportSize; - final contentSize = viewState.contentSize; - final scale = viewState.scale; - if (viewportSize == null || contentSize == null || contentSize.isEmpty || scale == null) return; + var region = _getVisibleRegion(context); + if (region == null) return; - final center = (contentSize / 2 - viewState.position / scale) as Size; - final regionSize = viewportSize / scale; - final regionTopLeft = (center - regionSize / 2) as Offset; - final region = Rect.fromLTWH(regionTopLeft.dx, regionTopLeft.dy, regionSize.width, regionSize.height); - final bytes = await _getBytes(context, scale, region); + if (useScrollEffect) { + final deltaX = min(region.left, entry.displaySize.width - region.right); + region = Rect.fromLTRB(region.left - deltaX, region.top, region.right + deltaX, region.bottom); + } + + final bytes = await _getBytes(context, region); final success = bytes != null && await WallpaperService.set(bytes, target); unawaited(reportController.close()); @@ -98,9 +91,25 @@ class WallpaperButtons extends StatelessWidget with FeedbackMixin { } } - Future _getBytes(BuildContext context, double scale, Rect displayRegion) async { + Rect? _getVisibleRegion(BuildContext context) { + final viewState = context.read().getOrCreateController(entry).value; + final viewportSize = viewState.viewportSize; + final contentSize = viewState.contentSize; + final scale = viewState.scale; + if (viewportSize == null || contentSize == null || contentSize.isEmpty || scale == null) return null; + + final center = (contentSize / 2 - viewState.position / scale) as Size; + final regionSize = viewportSize / scale; + final regionTopLeft = (center - regionSize / 2) as Offset; + return Rect.fromLTWH(regionTopLeft.dx, regionTopLeft.dy, regionSize.width, regionSize.height); + } + + Future _getBytes(BuildContext context, Rect displayRegion) async { + final viewState = context.read().getOrCreateController(entry).value; + final scale = viewState.scale; + final displaySize = entry.displaySize; - if (displaySize.isEmpty) return null; + if (displaySize.isEmpty || scale == null) return null; var storageRegion = Rectangle( displayRegion.left, diff --git a/lib/widgets/viewer/screen_saver_page.dart b/lib/widgets/viewer/screen_saver_page.dart index 036f9f179..7389cc793 100644 --- a/lib/widgets/viewer/screen_saver_page.dart +++ b/lib/widgets/viewer/screen_saver_page.dart @@ -43,6 +43,7 @@ class _ScreenSaverPageState extends State with WidgetsBindingOb repeat: true, autopilot: true, autopilotInterval: settings.screenSaverInterval.getDuration(), + autopilotAnimatedZoom: settings.screenSaverAnimatedZoomEffect, ); source.stateNotifier.addListener(_onSourceStateChanged); _initSlideshowCollection(); diff --git a/lib/widgets/viewer/slideshow_page.dart b/lib/widgets/viewer/slideshow_page.dart index d62be7afd..2d23769a5 100644 --- a/lib/widgets/viewer/slideshow_page.dart +++ b/lib/widgets/viewer/slideshow_page.dart @@ -45,6 +45,7 @@ class _SlideshowPageState extends State { repeat: settings.slideshowRepeat, autopilot: true, autopilotInterval: settings.slideshowInterval.getDuration(), + autopilotAnimatedZoom: settings.slideshowAnimatedZoomEffect, ); _initSlideshowCollection(); } diff --git a/lib/widgets/viewer/visual/controller_mixin.dart b/lib/widgets/viewer/visual/controller_mixin.dart index b00031a61..cc64293f8 100644 --- a/lib/widgets/viewer/visual/controller_mixin.dart +++ b/lib/widgets/viewer/visual/controller_mixin.dart @@ -66,7 +66,7 @@ mixin EntryViewControllerMixin on State { } } - bool _shouldAutoPlayVideo(BuildContext context) { + bool get videoAutoPlayEnabled { if (!isViewingImage) return false; switch (videoPlaybackOverride) { @@ -76,11 +76,39 @@ mixin EntryViewControllerMixin on State { case SlideshowVideoPlayback.playWithSound: return true; case null: - return settings.enableVideoAutoPlay; + break; + } + + switch (settings.videoAutoPlayMode) { + case VideoAutoPlayMode.disabled: + return false; + case VideoAutoPlayMode.playMuted: + case VideoAutoPlayMode.playWithSound: + return true; } } - bool _shouldAutoPlayMotionPhoto(BuildContext context) { + bool get shouldAutoPlayVideoMuted { + switch (videoPlaybackOverride) { + case SlideshowVideoPlayback.skip: + case SlideshowVideoPlayback.playWithSound: + return false; + case SlideshowVideoPlayback.playMuted: + return true; + case null: + break; + } + + switch (settings.videoAutoPlayMode) { + case VideoAutoPlayMode.disabled: + case VideoAutoPlayMode.playWithSound: + return false; + case VideoAutoPlayMode.playMuted: + return true; + } + } + + bool get shouldAutoPlayMotionPhoto { if (!isViewingImage) return false; return settings.enableMotionPhotoAutoPlay; @@ -90,7 +118,7 @@ mixin EntryViewControllerMixin on State { final controller = context.read().getOrCreateController(entry); setState(() {}); - if (_shouldAutoPlayVideo(context)) { + if (videoAutoPlayEnabled) { final resumeTimeMillis = await controller.getResumeTime(context); await _playVideo(controller, () => entry == entryNotifier.value, resumeTimeMillis: resumeTimeMillis); } @@ -117,7 +145,7 @@ mixin EntryViewControllerMixin on State { // auto play/pause when changing page Future _onPageChange() async { await pauseVideoControllers(); - if (_shouldAutoPlayVideo(context) || (entry.isMotionPhoto && _shouldAutoPlayMotionPhoto(context))) { + if (videoAutoPlayEnabled || (entry.isMotionPhoto && shouldAutoPlayMotionPhoto)) { final page = multiPageController.page; final pageInfo = multiPageInfo.getByIndex(page)!; if (pageInfo.isVideo) { @@ -135,7 +163,7 @@ mixin EntryViewControllerMixin on State { multiPageController.pageNotifier.addListener(_onPageChange); await _onPageChange(); - if (entry.isMotionPhoto && _shouldAutoPlayMotionPhoto(context)) { + if (entry.isMotionPhoto && shouldAutoPlayMotionPhoto) { await Future.delayed(Durations.motionPhotoAutoPlayDelay); if (entry == entryNotifier.value) { multiPageController.page = 1; @@ -160,7 +188,7 @@ mixin EntryViewControllerMixin on State { // so we play after a delay for increased stability await Future.delayed(const Duration(milliseconds: 300) * timeDilation); - if (videoPlaybackOverride == SlideshowVideoPlayback.playMuted && !videoController.isMuted) { + if (!videoController.isMuted && shouldAutoPlayVideoMuted) { await videoController.toggleMute(); } diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 911bddeb0..f767b8d67 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -16,6 +16,7 @@ import 'package:aves/widgets/common/magnifier/scale/scale_boundaries.dart'; import 'package:aves/widgets/common/magnifier/scale/scale_level.dart'; import 'package:aves/widgets/common/magnifier/scale/state.dart'; import 'package:aves/widgets/common/thumbnail/image.dart'; +import 'package:aves/widgets/viewer/controller.dart'; import 'package:aves/widgets/viewer/hero.dart'; import 'package:aves/widgets/viewer/notifications.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; @@ -35,7 +36,7 @@ import 'package:tuple/tuple.dart'; class EntryPageView extends StatefulWidget { final AvesEntry mainEntry, pageEntry; - final ScaleLevel initialScale; + final ViewerController viewerController; final VoidCallback? onDisposed; static const decorationCheckSize = 20.0; @@ -44,7 +45,7 @@ class EntryPageView extends StatefulWidget { super.key, required this.mainEntry, required this.pageEntry, - required this.initialScale, + required this.viewerController, this.onDisposed, }); @@ -52,7 +53,7 @@ class EntryPageView extends StatefulWidget { State createState() => _EntryPageViewState(); } -class _EntryPageViewState extends State { +class _EntryPageViewState extends State with SingleTickerProviderStateMixin { late ValueNotifier _viewStateNotifier; late MagnifierController _magnifierController; final List _subscriptions = []; @@ -72,9 +73,14 @@ class _EntryPageViewState extends State { AvesEntry get entry => widget.pageEntry; + ViewerController get viewerController => widget.viewerController; + // use the high res photo as cover for the video part of a motion photo ImageProvider get videoCoverUriImage => mainEntry.isMotionPhoto ? mainEntry.uriImage : entry.uriImage; + static const rasterMaxScale = ScaleLevel(factor: 5); + static const vectorMaxScale = ScaleLevel(factor: 25); + @override void initState() { super.initState(); @@ -109,9 +115,16 @@ class _EntryPageViewState extends State { _videoCoverStream = videoCoverUriImage.resolve(ImageConfiguration.empty); _videoCoverStream!.addListener(_videoCoverStreamListener); } + viewerController.startAutopilotAnimation( + vsync: this, + onUpdate: ({required scaleLevel}) { + final scale = _magnifierController.scaleBoundaries.scaleForLevel(scaleLevel); + _magnifierController.update(scale: scale, source: ChangeSource.animation); + }); } void _unregisterWidget(EntryPageView oldWidget) { + viewerController.stopAutopilotAnimation(vsync: this); _videoCoverStream?.removeListener(_videoCoverStreamListener); _videoCoverStream = null; _videoCoverInfoNotifier.value = null; @@ -174,7 +187,7 @@ class _EntryPageViewState extends State { Widget _buildSvgView() { return _buildMagnifier( - maxScale: const ScaleLevel(factor: 25), + maxScale: vectorMaxScale, scaleStateCycle: _vectorScaleStateCycle, applyScale: false, child: VectorImageView( @@ -365,7 +378,7 @@ class _EntryPageViewState extends State { Widget _buildMagnifier({ MagnifierController? controller, Size? displaySize, - ScaleLevel maxScale = const ScaleLevel(factor: 2.0), + ScaleLevel maxScale = rasterMaxScale, ScaleStateCycle scaleStateCycle = defaultScaleStateCycle, bool applyScale = true, MagnifierDoubleTapCallback? onDoubleTap, @@ -382,7 +395,7 @@ class _EntryPageViewState extends State { allowOriginalScaleBeyondRange: !isWallpaperMode, minScale: minScale, maxScale: maxScale, - initialScale: widget.initialScale, + initialScale: viewerController.initialScale, scaleStateCycle: scaleStateCycle, applyScale: applyScale, onTap: (c, s, a, p) => _onTap(alignment: a), diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index 45d77867b..7286676d9 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -116,7 +116,7 @@ class _WelcomePageState extends State { List _buildHeader(BuildContext context, {required bool isPortrait}) { final message = Text( context.l10n.welcomeMessage, - style: Theme.of(context).textTheme.headline5, + style: Theme.of(context).textTheme.headlineSmall, ); final padding = isPortrait ? 16.0 : 8.0; return [ diff --git a/plugins/aves_services_huawei/lib/aves_services_platform.dart b/plugins/aves_services_huawei/lib/aves_services_platform.dart index 528e29e1d..6648b9ae7 100644 --- a/plugins/aves_services_huawei/lib/aves_services_platform.dart +++ b/plugins/aves_services_huawei/lib/aves_services_platform.dart @@ -19,8 +19,12 @@ class PlatformMobileServices extends MobileServices { @override Future init() async { - final result = await HmsApiAvailability().isHMSAvailable(); - _isAvailable = result == _hmsCoreAvailable; + try { + final result = await HmsApiAvailability().isHMSAvailable(); + _isAvailable = result == _hmsCoreAvailable; + } on Exception catch (e, stack) { + debugPrint('Failed to check services availability with exception=$e, stack=$stack'); + } debugPrint('Device has Huawei Mobile Services=$_isAvailable'); final apiKey = await AvesPlatformMetaPlatform.instance.getMetadata(manifestApiKey); diff --git a/pubspec.lock b/pubspec.lock index ccfae1781..743fc4ada 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -8,6 +8,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "47.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" analyzer: dependency: transitive description: @@ -113,6 +120,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.1" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "5.7.7" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.10" collection: dependency: "direct main" description: @@ -126,7 +147,7 @@ packages: name: connectivity_plus url: "https://pub.dartlang.org" source: hosted - version: "2.3.7" + version: "2.3.9" connectivity_plus_linux: dependency: transitive description: @@ -140,7 +161,7 @@ packages: name: connectivity_plus_macos url: "https://pub.dartlang.org" source: hosted - version: "1.2.4" + version: "1.2.6" connectivity_plus_platform_interface: dependency: transitive description: @@ -154,7 +175,7 @@ packages: name: connectivity_plus_web url: "https://pub.dartlang.org" source: hosted - version: "1.2.3" + version: "1.2.5" connectivity_plus_windows: dependency: transitive description: @@ -217,42 +238,42 @@ packages: name: device_info_plus url: "https://pub.dartlang.org" source: hosted - version: "4.1.2" + version: "5.0.5" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "4.0.2" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "4.0.2" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "4.0.1" device_info_plus_web: dependency: transitive description: name: device_info_plus_web url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "4.0.2" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "5.0.2" dynamic_color: dependency: "direct main" description: @@ -319,7 +340,7 @@ packages: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "1.22.0" + version: "1.24.0" firebase_core_platform_interface: dependency: transitive description: @@ -333,21 +354,21 @@ packages: name: firebase_core_web url: "https://pub.dartlang.org" source: hosted - version: "1.7.2" + version: "1.7.3" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics url: "https://pub.dartlang.org" source: hosted - version: "2.8.10" + version: "2.8.13" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.2.16" + version: "3.2.19" flex_color_picker: dependency: "direct main" description: @@ -489,28 +510,28 @@ packages: name: google_maps_flutter url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.2.1" google_maps_flutter_android: dependency: transitive description: name: google_maps_flutter_android url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.3.2" google_maps_flutter_ios: dependency: transitive description: name: google_maps_flutter_ios url: "https://pub.dartlang.org" source: hosted - version: "2.1.11" + version: "2.1.12" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.2.2" + version: "2.2.4" highlight: dependency: transitive description: @@ -594,14 +615,14 @@ packages: name: logging url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.1.0" markdown: dependency: transitive description: name: markdown url: "https://pub.dartlang.org" source: hosted - version: "6.0.0" + version: "6.0.1" matcher: dependency: transitive description: @@ -647,9 +668,11 @@ packages: motion_sensors: dependency: transitive description: - name: motion_sensors - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: aves + resolved-ref: "0bf47932242dea4f93f51be0f6bfa3d9e5616c31" + url: "https://github.com/deckerst/aves_panorama_motion_sensors.git" + source: git version: "0.1.0" nested: dependency: transitive @@ -720,7 +743,7 @@ packages: name: package_info_plus_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.0.6" package_info_plus_windows: dependency: transitive description: @@ -740,7 +763,7 @@ packages: description: path: "." ref: aves - resolved-ref: "0050dc1aee451f821961980ea8b9c1eb3d131c01" + resolved-ref: "47442e5102a8bdf5e756d6dfab60108b73d132f0" url: "https://github.com/deckerst/aves_panorama.git" source: git version: "0.4.1" @@ -771,7 +794,7 @@ packages: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" path_provider_windows: dependency: transitive description: @@ -799,7 +822,7 @@ packages: name: permission_handler url: "https://pub.dartlang.org" source: hosted - version: "10.0.0" + version: "10.1.0" permission_handler_android: dependency: "direct overridden" description: @@ -815,21 +838,21 @@ packages: name: permission_handler_apple url: "https://pub.dartlang.org" source: hosted - version: "9.0.4" + version: "9.0.6" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.7.0" + version: "3.9.0" permission_handler_windows: dependency: transitive description: name: permission_handler_windows url: "https://pub.dartlang.org" source: hosted - version: "0.1.0" + version: "0.1.1" petitparser: dependency: transitive description: @@ -850,7 +873,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" polylabel: dependency: transitive description: @@ -914,20 +937,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.0" screen_brightness: dependency: "direct main" description: name: screen_brightness url: "https://pub.dartlang.org" source: hosted - version: "0.2.1" + version: "0.2.2" screen_brightness_android: dependency: transitive description: @@ -956,6 +972,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.0" + screen_brightness_windows: + dependency: transitive + description: + name: screen_brightness_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1" shared_preferences: dependency: "direct main" description: @@ -1018,7 +1041,7 @@ packages: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.3.2" + version: "1.4.0" shelf_packages_handler: dependency: transitive description: @@ -1045,6 +1068,13 @@ packages: description: flutter source: sdk version: "0.0.99" + smooth_page_indicator: + dependency: "direct main" + description: + name: smooth_page_indicator + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0+2" source_map_stack_trace: dependency: transitive description: @@ -1072,14 +1102,14 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.3+1" + version: "2.1.0+1" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.2.1+1" + version: "2.3.0" stack_trace: dependency: transitive description: @@ -1100,7 +1130,7 @@ packages: name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" streams_channel: dependency: "direct main" description: @@ -1172,7 +1202,7 @@ packages: name: tuple url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" typed_data: dependency: transitive description: @@ -1193,7 +1223,7 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.1.5" + version: "6.1.6" url_launcher_android: dependency: transitive description: @@ -1228,7 +1258,7 @@ packages: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" url_launcher_web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0864cf230..40f1c27eb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ repository: https://github.com/deckerst/aves # - github changelog: /CHANGELOG.md # - play changelog: /whatsnew/whatsnew-en-US # - izzy changelog: /fastlane/metadata/android/en-US/changelogs/1XXX.txt -version: 1.7.0+80 +version: 1.7.1+81 publish_to: none environment: @@ -75,6 +75,7 @@ dependencies: provider: screen_brightness: shared_preferences: + smooth_page_indicator: sqflite: streams_channel: git: @@ -85,7 +86,8 @@ dependencies: xml: dependency_overrides: - # TODO TLAD as of 2022/06/18, latest version (v10.0.0) does not support Android 13 storage permissions + # TODO TLAD as of 2022/10/09, latest version (v10.1.0) does not support Android 13 storage permissions + # `permission_handler_platform_interface` v3.9.0 added support for them but it is not effective permission_handler_android: git: url: https://github.com/deckerst/flutter-permission-handler @@ -153,22 +155,23 @@ flutter: # adapts from package `flutter_highlight` v0.7.0 # # `OutputBuffer` in `/services/common/output_buffer.dart` -# adapts from Flutter `_OutputBuffer` in `/foundation/consolidate_response.dart` +# adapts from Flutter v3.3.3 `_OutputBuffer` in `/foundation/consolidate_response.dart` # # `OverlaySnackBar` in `/widgets/common/action_mixins/overlay_snack_bar.dart` -# adapts from Flutter `SnackBar` in `/material/snack_bar.dart` +# adapts from Flutter v3.3.3 `SnackBar` in `/material/snack_bar.dart` # # `EagerScaleGestureRecognizer` in `/widgets/common/behaviour/eager_scale_gesture_recognizer.dart` -# adapts from Flutter `ScaleGestureRecognizer` in `/gestures/scale.dart` +# adapts from Flutter v3.3.3 `ScaleGestureRecognizer` in `/gestures/scale.dart` # # `KnownExtentScrollPhysics` in `/widgets/common/behaviour/known_extent_scroll_physics.dart` -# adapts from Flutter `FixedExtentScrollPhysics` in `/widgets/list_wheel_scroll_view.dart` +# adapts from Flutter v3.3.3 `FixedExtentScrollPhysics` in `/widgets/list_wheel_scroll_view.dart` # # `TransitionImage` in `/widgets/common/fx/transition_image.dart` -# adapts from Flutter `RawImage` in `/widgets/basic.dart` and `DecorationImagePainter` in `/painting/decoration_image.dart` +# adapts from Flutter v3.3.3 `_ImageState` in `/widgets/image.dart` +# and `DecorationImagePainter` in `/painting/decoration_image.dart` # # `_RenderSliverKnownExtentBoxAdaptor` in `/widgets/common/grid/sliver.dart` -# adapts from Flutter `RenderSliverFixedExtentBoxAdaptor` in `/rendering/sliver_fixed_extent_list.dart` +# adapts from Flutter v3.3.3 `RenderSliverFixedExtentBoxAdaptor` in `/rendering/sliver_fixed_extent_list.dart` # # `CollectionSearchDelegate`, `SearchPageRoute` in `/widgets/search/search_delegate.dart` -# adapts from Flutter `SearchDelegate`, `_SearchPageRoute` in `/material/search.dart` +# adapts from Flutter v3.3.3 `SearchDelegate`, `_SearchPageRoute` in `/material/search.dart` diff --git a/shaders_3.3.2.sksl.json b/shaders_3.3.2.sksl.json deleted file mode 100644 index fa4742cbf..000000000 --- a/shaders_3.3.2.sksl.json +++ /dev/null @@ -1 +0,0 @@ -{"platform":"android","name":"SM G970N","engineRevision":"a4ff2c53d84ca78702bc3be6c7ef0788ffca01d3","data":{"HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABGAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CQAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAAKQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","AYAA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1OCAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMl9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABZQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACnAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAADAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAYAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAIAAQAAAAAQGIA":"CQAAAExTS1PlAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc181X1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMSkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAAAAMAcAAHVuaWZvcm0gaGFsZjQgdXN0YXJ0X1MxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVlbmRfUzFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1MwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzFfY29vcmRzID0gX2Nvb3JkczsKCXJldHVybiBoYWxmNChtaXgodXN0YXJ0X1MxX2MwX2MwLCB1ZW5kX1MxX2MwX2MwLCBoYWxmKF90bXBfMV9jb29yZHMueCkpKTsKfQpoYWxmNCBMaW5lYXJMYXlvdXRfUzFfYzBfYzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8yX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8zX2Nvb3JkcyA9IHZUcmFuc2Zvcm1lZENvb3Jkc181X1MwOwoJcmV0dXJuIGhhbGY0KGhhbGY0KGhhbGYoX3RtcF8zX2Nvb3Jkcy54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDYsIDEuMCwgMC4wLCAwLjApKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckxheW91dF9TMV9jMF9jMV9jMChfaW5wdXQpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzRfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoX3RtcF80X2luQ29sb3IpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIWJvb2woaW50KDEpKSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKF90bXBfNF9pbkNvbG9yLCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglpZiAoYm9vbChpbnQoMSkpKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShoYWxmNCBfaW5wdXQpIAp7CglfaW5wdXQgPSBDbGFtcGVkR3JhZGllbnRfUzFfYzAoX2lucHV0KTsKCWhhbGY0IF90bXBfNV9pbkNvbG9yID0gX2lucHV0OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IGNvdmVyYWdlID0gdmNvdmVyYWdlX1MwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSAoaGFsZjQoMS4wKSAtIG91dHB1dF9TMSkgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CQAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAgBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdCB2Y292ZXJhZ2VfUzA7CmZsYXQgaW4gZmxvYXQ0IHZnZW9tU3Vic2V0X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQAAEAQAAAAGQCBAMQAAAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAIgDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzAueSwgdWNsYW1wX1MxX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACtBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","B2AAQAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CQAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cgl2aW5Db3ZlcmFnZV9TMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAZQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBoYWxmIHZpbkNvdmVyYWdlX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdUNvbG9yX1MwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAAA5AAAAAAABAAAAACAZAAAAA":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgAAAABdAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2YXJjY29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAEAAAAAAAAA","B2ABSAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CQAAAExTS1N4AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZiBpbkNvdmVyYWdlOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gdUNvbG9yX1MwOwoJY29sb3IgPSBjb2xvciAqIGluQ292ZXJhZ2U7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8zX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzFfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAeAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAyQEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGAARAGQWMHGBRIAAAAABQAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABhBAAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjbGVfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGQ7CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxLncpIC0gMS4wKSAqIHVjaXJjbGVfUzEueik7Cgl9CgllbHNlIAoJewoJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJaWYgKGludCgxKSA9PSBrRmlsbEFBX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIHNhdHVyYXRlKGQpKTsKCX0KCWVsc2UgCgl7CgkJcmV0dXJuIGhhbGY0KGQgPiAwLjUgPyBfaW5wdXQgOiBoYWxmNCgwLjApKTsKCX0KfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJZmxvYXQyIHRleENvb3JkOwoJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TMDsKCW91dHB1dENvbG9yX1MwID0gKChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHRleENvb3JkKSAqIGhhbGY0KDEpKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY2xlX1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAIAAEAAAABJYQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAEcGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TMV9jMF9jMF9jMC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueik7CgljbGFtcGVkQ29vcmQueSA9IHN1YnNldENvb3JkLnk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","EABQAAAAAEAAAAAQAABQAAIOAAABCFYIAAKAUDAAAAAAAAABAAAAAAAAAAANAAIAAAABAAAAACAJAAIAAAAA":"CQAAAExTS1OhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgZmxvYXQyIHZJbnRUZXh0dXJlQ29vcmRzX1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJdkludFRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkczsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8yX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEaXN0YW5jZUZpZWxkUGF0aAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfUzA7CgloYWxmNCB0ZXhDb2xvcjsKCXsKCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLnJycnI7Cgl9CgloYWxmIGRpc3RhbmNlID0gNy45Njg3NSoodGV4Q29sb3IuciAtIDAuNTAxOTYwNzg0MzEpOwoJaGFsZiBhZndpZHRoOwoJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeCh2SW50VGV4dHVyZUNvb3Jkc19TMC54KSkpOwoJaGFsZiB2YWwgPSBzbW9vdGhzdGVwKC1hZndpZHRoLCBhZndpZHRoLCBkaXN0YW5jZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KHZhbCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","DASAAAAAQAAWAABAYAAQBYH7777Z6QQBAEAAAAAAEAAAAAAAEBSAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1PVAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc1NpemVJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAD4AQAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfUzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHVDb2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCk7Cgl9CglvdXRwdXRDb2xvcl9TMCA9IG91dHB1dENvbG9yX1MwICogdGV4Q29sb3I7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACRAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","HVJAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAABAAAAAABBAMABAAOAAAABAAAAAAABBAMAAA":"CQAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAADoAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfUzApKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAEQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABPAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAACEA2X4PLOGEAAAAAAAAACAAAAAVQQAAQAAAAAQCDIBCAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAAAAAAA":"CQAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAACwQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBDaXJjbGVCbHVyX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TMS54eSkpICogZmxvYXQodWNpcmNsZURhdGFfUzEudykpOwoJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfUzEueikgKiB1Y2lyY2xlRGF0YV9TMS53OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIE1hdHJpeEVmZmVjdF9TMV9jMChfdG1wXzBfaW5Db2xvciwgZmxvYXQyKGhhbGYyKGRpc3QsIDAuNSkpKS53KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZUJsdXJfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAEAQAAAAGQCBAMQACAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAGUDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMC54eSwgdWNsYW1wX1MxX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CQAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAACzAgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIBAIAAAABLCIIBAAAAABAEGABBAMAACAIAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADhAwAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfUzFfYzBfYzAueHksIHVjbGFtcF9TMV9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoTWF0cml4RWZmZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvbG9yX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGACQAGAAAAAQAAAAAAAQQGAAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CQAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAACRAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKZmxhdCBpbiBmbG9hdDQgdmdlb21TdWJzZXRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAQAAAAAAAAA=","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAAEAQCAAAAAVREEAQAAAAAQCDAAQQGAABAEAAAAAAH4AQAAAAAEAAAAAQGIAAAAAAA":"CQAAAExTS1PfAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMCkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAEAFAAAjZXh0ZW5zaW9uIEdMX0VYVF9zaGFkZXJfZnJhbWVidWZmZXJfZmV0Y2g6IHJlcXVpcmUKdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1MwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cmlub3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMF9jMC54eSwgdWNsYW1wX1MxX2MwX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShoYWxmNCBfaW5wdXQpIAp7CglfaW5wdXQgPSBNYXRyaXhFZmZlY3RfUzFfYzAoX2lucHV0KTsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IGNvdmVyYWdlID0gdmNvdmVyYWdlX1MwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYgU2hhZGVyCgkJaGFsZjQgX2RzdENvbG9yID0gc2tfRnJhZ0NvbG9yOwoJCXNrX0ZyYWdDb2xvciA9IGJsZW5kX3NyYyhvdXRwdXRfUzEsIF9kc3RDb2xvcik7CgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q292ZXJhZ2VfUzAgKiBza19GcmFnQ29sb3IgKyAoaGFsZjQoMS4wKSAtIG91dHB1dENvdmVyYWdlX1MwKSAqIF9kc3RDb2xvcjsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAQAAAAAAAAAAAAAABAAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAGIBIAAABAAAAANAEAAAAAAAAAAAAAABAAOAAAABAAAAAAABBAMAAAAA":"CQAAAExTS1N0AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMl9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAIsCAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwKS5ycnJyOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAAAAIAAAABLAIABAAAAABAEGABBAMAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADOAgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TMV9jMChfc3JjKSwgX3NyYyk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb2xvcl9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAABAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAIAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIA":"CQAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAOMGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgwKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IENsYW1wZWRHcmFkaWVudF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF81X2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAB3QA6AAAEAAAAAAAMAAPEAEAAABAAAAAAB2AAAAAAACAAAAAEBSAAAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAA2BQAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzI7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MyOwppbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglhbHBoYSA9IDEuMCAtIGFscGhhOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CmhhbGY0IENpcmN1bGFyUlJlY3RfUzIoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MyLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MyLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzIueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCWhhbGY0IG91dHB1dF9TMjsKCW91dHB1dF9TMiA9IENpcmN1bGFyUlJlY3RfUzIob3V0cHV0X1MxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMjsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAACTAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAABAEAAAABJYQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAEcGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TMV9jMF9jMF9jMC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEADZABYAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAADsAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAEAAAAAAAAA","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CQAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAkAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","GEMAAAYAAEHAAAARC4EAAAQWBQAAAAAAAAAQAAAAIBCAAAGQAEAAAAAQAAAABAEQAEAAAAA":"CQAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAjAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAOAAAAaW5TaGFkb3dQYXJhbXMAAAEAAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAZCBRE4GNEACAAAOAAAAAAAAAAABAAOAAAABAAAAAAABBAMAA":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAAAHBQAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY2xlX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZiBkOwoJaWYgKGludCgzKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWQgPSBoYWxmKChsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSAtIDEuMCkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJZWxzZSAKCXsKCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1MxLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfUzEudykpICogdWNpcmNsZV9TMS56KTsKCX0KCWlmIChpbnQoMykgPT0ga0ZpbGxBQV9TMSB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCXJldHVybiBoYWxmNChfaW5wdXQgKiBzYXR1cmF0ZShkKSk7Cgl9CgllbHNlIAoJewoJCXJldHVybiBoYWxmNChkID4gMC41ID8gX2lucHV0IDogaGFsZjQoMC4wKSk7Cgl9Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZV9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAAAAEAAAABJYQAAAAAAAAIAAAAAWCBAAAABAAAAANAECAZAAAAAAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAAgFAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzA7CnVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TMV9jMDsKdW5pZm9ybSBoYWxmMiB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFsxM107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgX2Nvb3Jkcyk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChfaW5wdXQsIGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzApICogX2Nvb3Jkcy54eTEpOwp9CmhhbGY0IFNtb290aF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBjb29yZCwgaGFsZjIgb2Zmc2V0QW5kS2VybmVsKSAKewoJcmV0dXJuIE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChjb29yZCArIG9mZnNldEFuZEtlcm5lbC54ICogdUluY3JlbWVudF9TMV9jMCkpICogb2Zmc2V0QW5kS2VybmVsLnk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBjb2xvciA9IGhhbGY0KDApOwoJZmxvYXQyIGNvb3JkID0gdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cglmb3IgKGludCBpPTA7IGk8MTM7ICsraSkgCgl7CgkJY29sb3IgKz0gU21vb3RoX1MxX2MwKF9pbnB1dCwgY29vcmQsIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwW2ldKTsKCX0KCXJldHVybiBjb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HTQAAGAABBYAAAEIXBAAAGEAMAAAAAAAAAAAAAAAQAHAAAAAQAAAAAAAQQGAAAAA":"CQAAAExTS1M/AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgABAAAAHQMAAGluIGZsb2F0NCB2UXVhZEVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGYgZWRnZUFscGhhOwoJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1MwLnosIHZRdWFkRWRnZV9TMC53KSArIDAuNSwgMS4wKSk7Cgl9CgllbHNlIAoJewoJCWhhbGYyIGdGID0gaGFsZjIoaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TMC54KnZRdWFkRWRnZV9TMC54IC0gdlF1YWRFZGdlX1MwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIBSQB5VRECGAEAAAMAAAAAAAAAAACAA4AAAACAAAAAAACCAYAA":"CQAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAA4wQAAGNvbnN0IGludCBrRmlsbEJXX1MxID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGNvdmVyYWdlOwoJaWYgKGludCgxKSA9PSBrRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fUzEuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1MxLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7Cgl9CgllbHNlIAoJewoJCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1MxKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCWlmIChpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIGNvdmVyYWdlKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","BYIBQAAABQAAIAABBYAAAEIXBAAP777777777777AAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CQAAAExTS1M+AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzNfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAHgEAAGluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAEAAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAHSADQAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAAWAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAAcBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","AYTRVAADQAAAOAEARAFQJAABBADAAAILBYAACCYUQD777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7CmluIGhhbGYzIGluQ2xpcFBsYW5lOwppbiBoYWxmMyBpbklzZWN0UGxhbmU7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKb3V0IGhhbGYzIHZpbklzZWN0UGxhbmVfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5DbGlwUGxhbmVfUzAgPSBpbkNsaXBQbGFuZTsKCXZpbklzZWN0UGxhbmVfUzAgPSBpbklzZWN0UGxhbmU7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1MwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TMC55dzsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAD1AwAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKaW4gaGFsZjMgdmluSXNlY3RQbGFuZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGYzIGNsaXBQbGFuZTsKCWNsaXBQbGFuZSA9IHZpbkNsaXBQbGFuZV9TMDsKCWhhbGYzIGlzZWN0UGxhbmU7Cglpc2VjdFBsYW5lID0gdmluSXNlY3RQbGFuZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGYgY2xpcCA9IGhhbGYoc2F0dXJhdGUoY2lyY2xlRWRnZS56ICogZG90KGNpcmNsZUVkZ2UueHksIGNsaXBQbGFuZS54eSkgKyBjbGlwUGxhbmUueikpOwoJY2xpcCAqPSBoYWxmKHNhdHVyYXRlKGNpcmNsZUVkZ2UueiAqIGRvdChjaXJjbGVFZGdlLnh5LCBpc2VjdFBsYW5lLnh5KSArIGlzZWN0UGxhbmUueikpOwoJZWRnZUFscGhhICo9IGNsaXA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlCwAAAGluQ2xpcFBsYW5lAAwAAABpbklzZWN0UGxhbmUBAAAAAAAAAA=="}} \ No newline at end of file diff --git a/shaders_3.3.4.sksl.json b/shaders_3.3.4.sksl.json new file mode 100644 index 000000000..e335f6e5d --- /dev/null +++ b/shaders_3.3.4.sksl.json @@ -0,0 +1 @@ +{"platform":"android","name":"SM G970N","engineRevision":"c08d7d5efc9aa6eb3c30cfb3be6dc09bca5e7631","data":{"HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAEQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABPAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIBAIAAAABLCIIBAAAAABAEGABBAMAACAIAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADhAwAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfUzFfYzBfYzAueHksIHVjbGFtcF9TMV9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoTWF0cml4RWZmZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvbG9yX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CQAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAkAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CQAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAACRAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKZmxhdCBpbiBmbG9hdDQgdmdlb21TdWJzZXRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAQAAAAAAAAA=","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACtBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQAAEAQAAAAGQCBAMQAAAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAIgDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzAueSwgdWNsYW1wX1MxX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEADZABYAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAADsAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAEAAAAAAAAA","GEMAAAYAAEHAAAARC4EAAAQWBQAAAAAAAAAQAAAAIBCAAAGQAEAAAAAQAAAABAEQAEAAAAA":"CQAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAjAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAOAAAAaW5TaGFkb3dQYXJhbXMAAAEAAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIBSQB5VRECGAEAAAMAAAAAAAAAAACAA4AAAACAAAAAAACCAYAA":"CQAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAA4wQAAGNvbnN0IGludCBrRmlsbEJXX1MxID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGNvdmVyYWdlOwoJaWYgKGludCgxKSA9PSBrRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fUzEuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1MxLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7Cgl9CgllbHNlIAoJewoJCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1MxKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCWlmIChpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIGNvdmVyYWdlKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAKPABAAAAAAB2AAAAAAACAAAAAEBSAAAAAAA":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAACzBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBFbGxpcHRpY2FsUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CglmbG9hdDIgWiA9IGR4eSAqIHVpbnZSYWRpaVhZX1MxLnh5OwoJaGFsZiBpbXBsaWNpdCA9IGhhbGYoZG90KFosIGR4eSkgLSAxLjApOwoJaGFsZiBncmFkX2RvdCA9IGhhbGYoNC4wICogZG90KFosIFopKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjBlLTQpOwoJaGFsZiBhcHByb3hfZGlzdCA9IGltcGxpY2l0ICogaGFsZihpbnZlcnNlc3FydChncmFkX2RvdCkpOwoJaGFsZiBhbHBoYSA9IGNsYW1wKDAuNSArIGFwcHJveF9kaXN0LCAwLjAsIDEuMCk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEVsbGlwdGljYWxSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAABAEAAAABJYQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAEcGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TMV9jMF9jMF9jMC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAIAAEAAAABJYQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAEcGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1SW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGYyIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwWzEzXTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TMV9jMF9jMF9jMC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueik7CgljbGFtcGVkQ29vcmQueSA9IHN1YnNldENvb3JkLnk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBTbW9vdGhfUzFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgY29vcmQsIGhhbGYyIG9mZnNldEFuZEtlcm5lbCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoY29vcmQgKyBvZmZzZXRBbmRLZXJuZWwueCAqIHVJbmNyZW1lbnRfUzFfYzApKSAqIG9mZnNldEFuZEtlcm5lbC55Owp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgY29sb3IgPSBoYWxmNCgwKTsKCWZsb2F0MiBjb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgaT0wOyBpPDEzOyArK2kpIAoJewoJCWNvbG9yICs9IFNtb290aF9TMV9jMChfaW5wdXQsIGNvb3JkLCB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFtpXSk7Cgl9CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAACEA2X4PLOGEAAAAAAAAACAAAAAVQQAAQAAAAAQCDIBCAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAAAAAAA":"CQAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAACwQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBDaXJjbGVCbHVyX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TMS54eSkpICogZmxvYXQodWNpcmNsZURhdGFfUzEudykpOwoJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfUzEueikgKiB1Y2lyY2xlRGF0YV9TMS53OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIE1hdHJpeEVmZmVjdF9TMV9jMChfdG1wXzBfaW5Db2xvciwgZmxvYXQyKGhhbGYyKGRpc3QsIDAuNSkpKS53KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZUJsdXJfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAACTAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGAARAGQWMHGBRIAAAAABQAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABhBAAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjbGVfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGQ7CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxLncpIC0gMS4wKSAqIHVjaXJjbGVfUzEueik7Cgl9CgllbHNlIAoJewoJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJaWYgKGludCgxKSA9PSBrRmlsbEFBX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIHNhdHVyYXRlKGQpKTsKCX0KCWVsc2UgCgl7CgkJcmV0dXJuIGhhbGY0KGQgPiAwLjUgPyBfaW5wdXQgOiBoYWxmNCgwLjApKTsKCX0KfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJZmxvYXQyIHRleENvb3JkOwoJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TMDsKCW91dHB1dENvbG9yX1MwID0gKChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHRleENvb3JkKSAqIGhhbGY0KDEpKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY2xlX1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","AYTRVAADQAAAOAEARAFQJAABBADAAAILBYAACCYUQD777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7CmluIGhhbGYzIGluQ2xpcFBsYW5lOwppbiBoYWxmMyBpbklzZWN0UGxhbmU7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKb3V0IGhhbGYzIHZpbklzZWN0UGxhbmVfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5DbGlwUGxhbmVfUzAgPSBpbkNsaXBQbGFuZTsKCXZpbklzZWN0UGxhbmVfUzAgPSBpbklzZWN0UGxhbmU7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1MwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TMC55dzsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAD1AwAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKaW4gaGFsZjMgdmluSXNlY3RQbGFuZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGYzIGNsaXBQbGFuZTsKCWNsaXBQbGFuZSA9IHZpbkNsaXBQbGFuZV9TMDsKCWhhbGYzIGlzZWN0UGxhbmU7Cglpc2VjdFBsYW5lID0gdmluSXNlY3RQbGFuZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGYgY2xpcCA9IGhhbGYoc2F0dXJhdGUoY2lyY2xlRWRnZS56ICogZG90KGNpcmNsZUVkZ2UueHksIGNsaXBQbGFuZS54eSkgKyBjbGlwUGxhbmUueikpOwoJY2xpcCAqPSBoYWxmKHNhdHVyYXRlKGNpcmNsZUVkZ2UueiAqIGRvdChjaXJjbGVFZGdlLnh5LCBpc2VjdFBsYW5lLnh5KSArIGlzZWN0UGxhbmUueikpOwoJZWRnZUFscGhhICo9IGNsaXA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlCwAAAGluQ2xpcFBsYW5lAAwAAABpbklzZWN0UGxhbmUBAAAAAAAAAA==","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CQAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAAKQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAyQEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABZQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACnAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HVJAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAABAAAAAABBAMABAAOAAAABAAAAAAABBAMAAA":"CQAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAADoAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfUzApKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","BYIBQAAABQAAIAABBYAAAEIXBAAP777777777777AAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CQAAAExTS1M+AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzNfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAHgEAAGluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAEAAAAAAAAA","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAABAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAIAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIA":"CQAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAOMGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgwKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IENsYW1wZWRHcmFkaWVudF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF81X2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAGIBIAAABAAAAANAEAAAAAAAAAAAAAABAAOAAAABAAAAAAABBAMAAAAA":"CQAAAExTS1N0AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMl9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAIsCAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwKS5ycnJyOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","B2AAQAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CQAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cgl2aW5Db3ZlcmFnZV9TMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAZQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBoYWxmIHZpbkNvdmVyYWdlX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdUNvbG9yX1MwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","AYAA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1OCAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMl9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAAcBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAEAQAAAAGQCBAMQACAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAGUDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMC54eSwgdWNsYW1wX1MxX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAHSADQAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAAWAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACRAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","B2ABSAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CQAAAExTS1N4AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZiBpbkNvdmVyYWdlOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gdUNvbG9yX1MwOwoJY29sb3IgPSBjb2xvciAqIGluQ292ZXJhZ2U7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8zX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzFfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAeAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","EABQAAAAAEAAAAAQAABQAAIOAAABCFYIAAKAUDAAAAAAAAABAAAAAAAAAAANAAIAAAABAAAAACAJAAIAAAAA":"CQAAAExTS1OhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgZmxvYXQyIHZJbnRUZXh0dXJlQ29vcmRzX1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJdkludFRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkczsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8yX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEaXN0YW5jZUZpZWxkUGF0aAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfUzA7CgloYWxmNCB0ZXhDb2xvcjsKCXsKCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLnJycnI7Cgl9CgloYWxmIGRpc3RhbmNlID0gNy45Njg3NSoodGV4Q29sb3IuciAtIDAuNTAxOTYwNzg0MzEpOwoJaGFsZiBhZndpZHRoOwoJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeCh2SW50VGV4dHVyZUNvb3Jkc19TMC54KSkpOwoJaGFsZiB2YWwgPSBzbW9vdGhzdGVwKC1hZndpZHRoLCBhZndpZHRoLCBkaXN0YW5jZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KHZhbCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAZCBRE4GNEACAAAOAAAAAAAAAAABAAOAAAABAAAAAAABBAMAA":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAAAHBQAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY2xlX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZiBkOwoJaWYgKGludCgzKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWQgPSBoYWxmKChsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSAtIDEuMCkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJZWxzZSAKCXsKCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1MxLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfUzEudykpICogdWNpcmNsZV9TMS56KTsKCX0KCWlmIChpbnQoMykgPT0ga0ZpbGxBQV9TMSB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCXJldHVybiBoYWxmNChfaW5wdXQgKiBzYXR1cmF0ZShkKSk7Cgl9CgllbHNlIAoJewoJCXJldHVybiBoYWxmNChkID4gMC41ID8gX2lucHV0IDogaGFsZjQoMC4wKSk7Cgl9Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZV9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGACQAGAAAAAQAAAAAAAQQGAAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAB3QA6AAAEAAAAAAAMAAPEAEAAABAAAAAAB2AAAAAAACAAAAAEBSAAAA":"CQAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAA2BQAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzI7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MyOwppbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglhbHBoYSA9IDEuMCAtIGFscGhhOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CmhhbGY0IENpcmN1bGFyUlJlY3RfUzIoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MyLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MyLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzIueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCWhhbGY0IG91dHB1dF9TMjsKCW91dHB1dF9TMiA9IENpcmN1bGFyUlJlY3RfUzIob3V0cHV0X1MxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMjsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAAAAIAAAABLAIABAAAAABAEGABBAMAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADOAgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TMV9jMChfc3JjKSwgX3NyYyk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb2xvcl9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAAA5AAAAAAABAAAAACAZAAAAA":"CQAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgAAAABdAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2YXJjY29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABAAAAHNrZXcZAAAAdHJhbnNsYXRlX2FuZF9sb2NhbHJvdGF0ZQAAAAUAAABjb2xvcgAAAAEAAAAAAAAA","HTQAAGAABBYAAAEIXBAAAGEAMAAAAAAAAAAAAAAAQAHAAAAAQAAAAAAAQQGAAAAA":"CQAAAExTS1M/AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgABAAAAHQMAAGluIGZsb2F0NCB2UXVhZEVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGYgZWRnZUFscGhhOwoJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1MwLnosIHZRdWFkRWRnZV9TMC53KSArIDAuNSwgMS4wKSk7Cgl9CgllbHNlIAoJewoJCWhhbGYyIGdGID0gaGFsZjIoaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TMC54KnZRdWFkRWRnZV9TMC54IC0gdlF1YWRFZGdlX1MwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAAEAQCAAAAAVREEAQAAAAAQCDAAQQGAABAEAAAAAAH4AQAAAAAEAAAAAQGIAAAAAAA":"CQAAAExTS1PfAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMCkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAEAFAAAjZXh0ZW5zaW9uIEdMX0VYVF9zaGFkZXJfZnJhbWVidWZmZXJfZmV0Y2g6IHJlcXVpcmUKdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1MwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cmlub3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMF9jMC54eSwgdWNsYW1wX1MxX2MwX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShoYWxmNCBfaW5wdXQpIAp7CglfaW5wdXQgPSBNYXRyaXhFZmZlY3RfUzFfYzAoX2lucHV0KTsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IGNvdmVyYWdlID0gdmNvdmVyYWdlX1MwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYgU2hhZGVyCgkJaGFsZjQgX2RzdENvbG9yID0gc2tfRnJhZ0NvbG9yOwoJCXNrX0ZyYWdDb2xvciA9IGJsZW5kX3NyYyhvdXRwdXRfUzEsIF9kc3RDb2xvcik7CgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q292ZXJhZ2VfUzAgKiBza19GcmFnQ29sb3IgKyAoaGFsZjQoMS4wKSAtIG91dHB1dENvdmVyYWdlX1MwKSAqIF9kc3RDb2xvcjsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAQAAAAAAAAAAAAAABAAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CQAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABGAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CQAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAgBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdCB2Y292ZXJhZ2VfUzA7CmZsYXQgaW4gZmxvYXQ0IHZnZW9tU3Vic2V0X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAAcG9zaXRpb24IAAAAY292ZXJhZ2UFAAAAY29sb3IAAAAKAAAAZ2VvbVN1YnNldAAAAQAAAAAAAAA=","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CQAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAACzAgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","DBAAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAIAAAAAAAAAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CQAAAExTS1NVAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludDIgY29vcmRzID0gaW50MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJaW50IHRleElkeCA9IGNvb3Jkcy54ID4+IDEzOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGNvb3Jkcy54ICYgMHgxRkZGLCBjb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAAFoCAAB1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzFfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7CglpZiAodlRleEluZGV4X1MwID09IDApIAoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWVsc2UgCgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzFfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAADAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAYAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAIAAQAAAAAQGIA":"CQAAAExTS1PlAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc181X1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMSkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAAAAMAcAAHVuaWZvcm0gaGFsZjQgdXN0YXJ0X1MxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVlbmRfUzFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1MwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzFfY29vcmRzID0gX2Nvb3JkczsKCXJldHVybiBoYWxmNChtaXgodXN0YXJ0X1MxX2MwX2MwLCB1ZW5kX1MxX2MwX2MwLCBoYWxmKF90bXBfMV9jb29yZHMueCkpKTsKfQpoYWxmNCBMaW5lYXJMYXlvdXRfUzFfYzBfYzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8yX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8zX2Nvb3JkcyA9IHZUcmFuc2Zvcm1lZENvb3Jkc181X1MwOwoJcmV0dXJuIGhhbGY0KGhhbGY0KGhhbGYoX3RtcF8zX2Nvb3Jkcy54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDYsIDEuMCwgMC4wLCAwLjApKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckxheW91dF9TMV9jMF9jMV9jMChfaW5wdXQpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzRfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoX3RtcF80X2luQ29sb3IpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIWJvb2woaW50KDEpKSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKF90bXBfNF9pbkNvbG9yLCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglpZiAoYm9vbChpbnQoMSkpKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShoYWxmNCBfaW5wdXQpIAp7CglfaW5wdXQgPSBDbGFtcGVkR3JhZGllbnRfUzFfYzAoX2lucHV0KTsKCWhhbGY0IF90bXBfNV9pbkNvbG9yID0gX2lucHV0OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IGNvdmVyYWdlID0gdmNvdmVyYWdlX1MwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSAoaGFsZjQoMS4wKSAtIG91dHB1dF9TMSkgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAAAAEAAAABJYQAAAAAAAAIAAAAAWCBAAAABAAAAANAECAZAAAAAAAAAAAFAAMAAAABAAAAAAABBAM":"CQAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAAgFAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzA7CnVuaWZvcm0gaGFsZjIgdUluY3JlbWVudF9TMV9jMDsKdW5pZm9ybSBoYWxmMiB1T2Zmc2V0c0FuZEtlcm5lbF9TMV9jMFsxM107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgX2Nvb3Jkcyk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChfaW5wdXQsIGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzApICogX2Nvb3Jkcy54eTEpOwp9CmhhbGY0IFNtb290aF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBjb29yZCwgaGFsZjIgb2Zmc2V0QW5kS2VybmVsKSAKewoJcmV0dXJuIE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChjb29yZCArIG9mZnNldEFuZEtlcm5lbC54ICogdUluY3JlbWVudF9TMV9jMCkpICogb2Zmc2V0QW5kS2VybmVsLnk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBjb2xvciA9IGhhbGY0KDApOwoJZmxvYXQyIGNvb3JkID0gdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cglmb3IgKGludCBpPTA7IGk8MTM7ICsraSkgCgl7CgkJY29sb3IgKz0gU21vb3RoX1MxX2MwKF9pbnB1dCwgY29vcmQsIHVPZmZzZXRzQW5kS2VybmVsX1MxX2MwW2ldKTsKCX0KCXJldHVybiBjb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","DASAAAAAQAAWAABAYAAQBYH7777Z6QQBAEAAAAAAEAAAAAAAEBSAAAB2AAAAAAACAAAAAEBSAAAAA":"CQAAAExTS1PVAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc1NpemVJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAD4AQAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfUzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHVDb2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCk7Cgl9CglvdXRwdXRDb2xvcl9TMCA9IG91dHB1dENvbG9yX1MwICogdGV4Q29sb3I7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA=="}} \ No newline at end of file diff --git a/test/fake/metadata_db.dart b/test/fake/metadata_db.dart index 49e36feba..1d8fdc1a1 100644 --- a/test/fake/metadata_db.dart +++ b/test/fake/metadata_db.dart @@ -15,9 +15,6 @@ class FakeMetadataDb extends Fake implements MetadataDb { @override int get nextId => ++_lastId; - @override - int get timestampSecs => DateTime.now().millisecondsSinceEpoch ~/ 1000; - @override Future init() => SynchronousFuture(null); diff --git a/test/model/collection_source_test.dart b/test/model/collection_source_test.dart index 9858c8114..033a3bdde 100644 --- a/test/model/collection_source_test.dart +++ b/test/model/collection_source_test.dart @@ -113,7 +113,7 @@ void main() { }); test('album/country/tag hidden on launch when their items are hidden by entry prop', () async { - settings.hiddenFilters = {const AlbumFilter(testAlbum, 'whatever')}; + settings.hiddenFilters = {AlbumFilter(testAlbum, 'whatever')}; final image1 = FakeMediaStoreService.newImage(testAlbum, 'image1'); (mediaStoreService as FakeMediaStoreService).entries = { @@ -191,7 +191,7 @@ void main() { expect(source.rawAlbums.length, 1); expect(covers.count, 0); - const albumFilter = AlbumFilter(testAlbum, 'whatever'); + final albumFilter = AlbumFilter(testAlbum, 'whatever'); expect(albumFilter.test(image1), true); expect(covers.count, 0); expect(covers.of(albumFilter), null); @@ -213,7 +213,7 @@ void main() { final source = await _initSource(); await image1.toggleFavourite(); - const albumFilter = AlbumFilter(testAlbum, 'whatever'); + final albumFilter = AlbumFilter(testAlbum, 'whatever'); await covers.set(filter: albumFilter, entryId: image1.id, packageName: null, color: null); await source.updateAfterRename( todoEntries: {image1}, @@ -257,8 +257,8 @@ void main() { expect(source.rawAlbums.contains(sourceAlbum), true); expect(source.rawAlbums.contains(destinationAlbum), false); - const sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever'); - const destinationAlbumFilter = AlbumFilter(destinationAlbum, 'whatever'); + final sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever'); + final destinationAlbumFilter = AlbumFilter(destinationAlbum, 'whatever'); expect(sourceAlbumFilter.test(image1), true); expect(destinationAlbumFilter.test(image1), false); @@ -308,7 +308,7 @@ void main() { final source = await _initSource(); expect(source.rawAlbums.length, 1); - const sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever'); + final sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever'); await covers.set(filter: sourceAlbumFilter, entryId: image1.id, packageName: null, color: null); await source.updateAfterMove( @@ -333,14 +333,14 @@ void main() { final source = await _initSource(); await image1.toggleFavourite(); - var albumFilter = const AlbumFilter(sourceAlbum, 'whatever'); + var albumFilter = AlbumFilter(sourceAlbum, 'whatever'); await covers.set(filter: albumFilter, entryId: image1.id, packageName: null, color: null); await source.renameAlbum(sourceAlbum, destinationAlbum, { image1 }, { FakeMediaStoreService.moveOpEventForMove(image1, sourceAlbum, destinationAlbum), }); - albumFilter = const AlbumFilter(destinationAlbum, 'whatever'); + albumFilter = AlbumFilter(destinationAlbum, 'whatever'); expect(favourites.count, 1); expect(image1.isFavourite, true); diff --git a/test/model/filters_test.dart b/test/model/filters_test.dart index 39e6f582f..be4627b73 100644 --- a/test/model/filters_test.dart +++ b/test/model/filters_test.dart @@ -33,7 +33,7 @@ void main() { test('Filter serialization', () { CollectionFilter? jsonRoundTrip(filter) => CollectionFilter.fromJson(filter.toJson()); - const album = AlbumFilter('path/to/album', 'album'); + final album = AlbumFilter('path/to/album', 'album'); expect(album, jsonRoundTrip(album)); final bounds = CoordinateFilter(LatLng(29.979167, 28.223615), LatLng(36.451000, 31.134167)); @@ -63,7 +63,7 @@ void main() { final query = QueryFilter('some query'); expect(query, jsonRoundTrip(query)); - const rating = RatingFilter(3); + final rating = RatingFilter(3); expect(rating, jsonRoundTrip(rating)); final recent = RecentlyAddedFilter.instance; diff --git a/test/model/video/metadata_test.dart b/test/model/video/metadata_test.dart index 6983fbac8..833621c23 100644 --- a/test/model/video/metadata_test.dart +++ b/test/model/video/metadata_test.dart @@ -9,7 +9,7 @@ void main() { expect(VideoMetadataFormatter.parseVideoDate('UTC 2021-05-30 19:14:21'), DateTime(2021, 5, 30, 19, 14, 21).millisecondsSinceEpoch); expect(VideoMetadataFormatter.parseVideoDate('2021/10/31 21:23:17'), DateTime(2021, 10, 31, 21, 23, 17).millisecondsSinceEpoch); expect(VideoMetadataFormatter.parseVideoDate('2021-09-10T7:14:49 pmZ'), DateTime(2021, 9, 10, 19, 14, 49).millisecondsSinceEpoch); - expect(VideoMetadataFormatter.parseVideoDate('2022-01-28T5:07:46 p. m.Z'), DateTime(2022, 1, 28, 5, 7, 46).millisecondsSinceEpoch); + expect(VideoMetadataFormatter.parseVideoDate('2022-01-28T5:07:46 p. m.Z'), DateTime(2022, 1, 28, 17, 7, 46).millisecondsSinceEpoch); expect(VideoMetadataFormatter.parseVideoDate('2012-1-1T12:00:00Z'), DateTime(2012, 1, 1, 12, 0, 0).millisecondsSinceEpoch); expect(VideoMetadataFormatter.parseVideoDate('2020.10.14'), DateTime(2020, 10, 14).millisecondsSinceEpoch); }); diff --git a/test_driver/driver_screenshots.dart b/test_driver/driver_screenshots.dart index 2c2e11050..abda8fdee 100644 --- a/test_driver/driver_screenshots.dart +++ b/test_driver/driver_screenshots.dart @@ -1,8 +1,10 @@ import 'package:aves/main_play.dart' as app; +import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -17,6 +19,8 @@ Future configureAndLaunch() async { ..hasAcceptedTerms = true ..isInstalledAppAccessAllowed = true ..isErrorReportingAllowed = false + ..setTileExtent(CollectionPage.routeName, 69) + ..setTileLayout(CollectionPage.routeName, TileLayout.mosaic) ..setTileExtent(CountryListPage.routeName, 112) ..setTileLayout(CountryListPage.routeName, TileLayout.grid) // display @@ -28,6 +32,7 @@ Future configureAndLaunch() async { ..keepScreenOn = KeepScreenOn.always ..homePage = HomePageSetting.collection ..enableBottomNavigationBar = true + ..drawerTypeBookmarks = [null, FavouriteFilter.instance] // collection ..collectionSectionFactor = EntryGroupFactor.month ..collectionSortFactor = EntrySortFactor.date diff --git a/untranslated.json b/untranslated.json index 3675a7374..e034aad2b 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,26 +1,16 @@ { - "de": [ - "entryInfoActionEditTitleDescription", - "filterNoDateLabel", - "filterNoTitleLabel", - "viewDialogReverseSortOrder", - "sortOrderNewestFirst", - "sortOrderOldestFirst", - "sortOrderAtoZ", - "sortOrderZtoA", - "sortOrderHighestFirst", - "sortOrderLowestFirst", - "sortOrderLargestFirst", - "sortOrderSmallestFirst", - "searchMetadataSectionTitle" - ], - "es": [ + "chipActionFilterOut", + "chipActionFilterIn", "entryInfoActionEditTitleDescription", "filterNoDateLabel", "filterNoTitleLabel", "filterRecentlyAddedLabel", + "viewerTransitionNone", + "widgetOpenPageHome", + "widgetOpenPageViewer", "viewDialogReverseSortOrder", + "tileLayoutMosaic", "sortOrderNewestFirst", "sortOrderOldestFirst", "sortOrderAtoZ", @@ -29,17 +19,46 @@ "sortOrderLowestFirst", "sortOrderLargestFirst", "sortOrderSmallestFirst", + "albumGroupType", + "albumMimeTypeMixed", "searchMetadataSectionTitle", + "settingsDisabled", "settingsConfirmationAfterMoveToBinItems", - "viewerInfoLabelDescription" + "settingsSlideshowAnimatedZoomEffect", + "settingsWidgetOpenPage", + "statsTopAlbumsSectionTitle", + "viewerInfoLabelDescription", + "wallpaperUseScrollEffect" + ], + + "id": [ + "chipActionFilterOut", + "chipActionFilterIn", + "viewerTransitionNone", + "widgetOpenPageHome", + "widgetOpenPageViewer", + "tileLayoutMosaic", + "albumGroupType", + "albumMimeTypeMixed", + "settingsDisabled", + "settingsSlideshowAnimatedZoomEffect", + "settingsWidgetOpenPage", + "statsTopAlbumsSectionTitle", + "wallpaperUseScrollEffect" ], "ja": [ + "chipActionFilterOut", + "chipActionFilterIn", "entryInfoActionEditTitleDescription", "filterNoDateLabel", "filterNoTitleLabel", "filterRecentlyAddedLabel", + "viewerTransitionNone", + "widgetOpenPageHome", + "widgetOpenPageViewer", "viewDialogReverseSortOrder", + "tileLayoutMosaic", "sortOrderNewestFirst", "sortOrderOldestFirst", "sortOrderAtoZ", @@ -48,29 +67,22 @@ "sortOrderLowestFirst", "sortOrderLargestFirst", "sortOrderSmallestFirst", + "albumGroupType", + "albumMimeTypeMixed", "searchMetadataSectionTitle", + "settingsDisabled", "settingsConfirmationAfterMoveToBinItems", "settingsViewerGestureSideTapNext", - "viewerInfoLabelDescription" - ], - - "pt": [ - "entryInfoActionEditTitleDescription", - "filterNoDateLabel", - "filterNoTitleLabel", - "viewDialogReverseSortOrder", - "sortOrderNewestFirst", - "sortOrderOldestFirst", - "sortOrderAtoZ", - "sortOrderZtoA", - "sortOrderHighestFirst", - "sortOrderLowestFirst", - "sortOrderLargestFirst", - "sortOrderSmallestFirst", - "searchMetadataSectionTitle" + "settingsSlideshowAnimatedZoomEffect", + "settingsWidgetOpenPage", + "statsTopAlbumsSectionTitle", + "viewerInfoLabelDescription", + "wallpaperUseScrollEffect" ], "tr": [ + "chipActionFilterOut", + "chipActionFilterIn", "slideshowActionResume", "slideshowActionShowInCollection", "entryInfoActionEditTitleDescription", @@ -78,18 +90,22 @@ "filterNoTitleLabel", "filterOnThisDayLabel", "filterRecentlyAddedLabel", - "slideshowVideoPlaybackSkip", - "slideshowVideoPlaybackMuted", - "slideshowVideoPlaybackWithSound", + "videoPlaybackSkip", + "videoPlaybackMuted", + "videoPlaybackWithSound", "viewerTransitionSlide", "viewerTransitionParallax", "viewerTransitionFade", "viewerTransitionZoomIn", + "viewerTransitionNone", "wallpaperTargetHome", "wallpaperTargetLock", "wallpaperTargetHomeLock", + "widgetOpenPageHome", + "widgetOpenPageViewer", "menuActionSlideshow", "viewDialogReverseSortOrder", + "tileLayoutMosaic", "sortOrderNewestFirst", "sortOrderOldestFirst", "sortOrderAtoZ", @@ -98,7 +114,10 @@ "sortOrderLowestFirst", "sortOrderLargestFirst", "sortOrderSmallestFirst", + "albumGroupType", + "albumMimeTypeMixed", "searchMetadataSectionTitle", + "settingsDisabled", "settingsConfirmationAfterMoveToBinItems", "settingsViewerGestureSideTapNext", "settingsViewerSlideshowTile", @@ -106,15 +125,17 @@ "settingsSlideshowRepeat", "settingsSlideshowShuffle", "settingsSlideshowFillScreen", + "settingsSlideshowAnimatedZoomEffect", "settingsSlideshowTransitionTile", - "settingsSlideshowTransitionDialogTitle", "settingsSlideshowIntervalTile", - "settingsSlideshowIntervalDialogTitle", "settingsSlideshowVideoPlaybackTile", "settingsSlideshowVideoPlaybackDialogTitle", "settingsScreenSaverPageTitle", "settingsWidgetShowOutline", + "settingsWidgetOpenPage", + "statsTopAlbumsSectionTitle", "viewerSetWallpaperButtonLabel", - "viewerInfoLabelDescription" + "viewerInfoLabelDescription", + "wallpaperUseScrollEffect" ] } diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index af4200674..e5b6ac839 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,5 +1,5 @@ -In v1.7.0: -- change the sort order -- edit image titles -- enjoy the app in Greek +In v1.7.1: +- view your photos with the mosaic layout +- reverse filters to filter out/in +- set wallpapers with scroll effect Full changelog available on GitHub \ No newline at end of file