diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 7b9ac7f6f..483894b13 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -17,14 +17,15 @@ jobs: # Available versions may lag behind https://github.com/flutter/flutter.git - uses: subosito/flutter-action@v2 with: - flutter-version: '2.10.4' + flutter-version: '3.0.1' channel: 'stable' - name: Clone the repository. uses: actions/checkout@v2 - name: Get packages for the Flutter project. - run: flutter pub get + working-directory: ${{ github.workspace }}/scripts + run: ./pub_get_all.sh - name: Update the flutter version file. working-directory: ${{ github.workspace }}/scripts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b0d4edd1..cae6d2637 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: '2.10.4' + flutter-version: '3.0.1' channel: 'stable' # Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1): @@ -31,7 +31,8 @@ jobs: uses: actions/checkout@v2 - name: Get packages for the Flutter project. - run: flutter pub get + working-directory: ${{ github.workspace }}/scripts + run: ./pub_get_all.sh - name: Update the flutter version file. working-directory: ${{ github.workspace }}/scripts @@ -55,12 +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_2.10.4.sksl.json + flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.0.1.sksl.json cp build/app/outputs/bundle/playRelease/*.aab outputs - flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_2.10.4.sksl.json + flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.0.1.sksl.json cp build/app/outputs/apk/play/release/*.apk outputs + (cd scripts/; ./apply_flavor_huawei.sh) + flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_3.0.1.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_2.10.4.sksl.json + flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_3.0.1.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 595b4ebeb..82610377d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [v1.6.5] - 2022-05-25 + +### Added + +- Bottom navigation bar +- Collection: thumbnail overlay tag icon +- Collection: fast-scrolling shows breadcrumbs from groups +- Settings: search +- Pick: allow selecting multiple items according to request intent +- `huawei` app flavor (Petal Maps, no Crashlytics) + +### Changed + +- upgraded Flutter to stable v3.0.1 +- stretching overscroll effect +- disabled Google Maps layer on Android Lollipop + +### Fixed + +- grey Google Map layer when size changed +- Android scrolling screenshot support +- Voice Access scrolling support + ## [v1.6.4] - 2022-04-19 ### Added diff --git a/README.md b/README.md index c891c9f2c..271e29520 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,17 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt [Get it on Google Play](https://play.google.com/store/apps/details?id=deckers.thibault.aves&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1) +[Get it on Amazon Appstore](https://www.amazon.com/dp/B09XQHQQ72) [Get it on IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/deckers.thibault.aves) [Get it on GitHub](https://github.com/deckerst/aves/releases/latest) + +[Compare versions](https://github.com/deckerst/aves/wiki/App-Versions)
@@ -77,7 +82,7 @@ Aves requires a few permissions to do its job: ### Issues -[Bug reports](https://github.com/deckerst/aves/issues/new?assignees=&labels=type%3Abug&template=bug_report.md&title=) and [feature requests](https://github.com/deckerst/aves/issues/new?assignees=&labels=type%3Afeature&template=feature_request.md&title=) are welcome. Questions too, though you could also ask them in [Discussions](https://github.com/deckerst/aves/discussions). +[Bug reports](https://github.com/deckerst/aves/issues/new?assignees=&labels=type%3Abug&template=bug_report.md&title=) and [feature requests](https://github.com/deckerst/aves/issues/new?assignees=&labels=type%3Afeature&template=feature_request.md&title=) are welcome, but read the [guidelines](https://github.com/deckerst/aves/issues/234) first. If you have questions, check out the [discussions](https://github.com/deckerst/aves/discussions). ### Code diff --git a/analysis_options.yaml b/analysis_options.yaml index bfd8aadb8..d4f4380d2 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -11,6 +11,12 @@ analyzer: linter: rules: + # from 'flutter_lints', excluded + use_build_context_synchronously: false # no alternative + + # from 'lints / recommended', excluded + no_leading_underscores_for_local_identifiers: false # useful for null checked variable variants + # from 'effective dart', excluded avoid_classes_with_only_static_members: false # too strict avoid_function_literals_in_foreach_calls: false # benefit? diff --git a/android/app/agconnect-services.json b/android/app/agconnect-services.json new file mode 100644 index 000000000..876ecb775 --- /dev/null +++ b/android/app/agconnect-services.json @@ -0,0 +1,75 @@ +{ + "agcgw_all":{ + "CN":"connect-drcn.dbankcloud.cn", + "CN_back":"connect-drcn.hispace.hicloud.com", + "DE":"connect-dre.dbankcloud.cn", + "DE_back":"connect-dre.hispace.hicloud.com", + "RU":"connect-drru.hispace.dbankcloud.ru", + "RU_back":"connect-drru.hispace.dbankcloud.ru", + "SG":"connect-dra.dbankcloud.cn", + "SG_back":"connect-dra.hispace.hicloud.com" + }, + "client":{ + "cp_id":"2640082000020010713", + "product_id":"99536292102197525", + "client_id":"874325707927340288", + "client_secret":"DCAFAE5C0440ABDBD6DDB2B6EBD7D9B0870C10FCA64759CCD63020D168803AB5", + "project_id":"99536292102197525", + "app_id":"106014023", + "api_key":"DAEDAEzScQA5ri36P2NEiVPSFrOJeYZ0DbEJZMGJrBadW+QudBr5BGHD3vO0tsL1VeBy0RPZefPic3hAWUijcBxCv0zRv0iBjQEptQ==", + "package_name":"deckers.thibault.aves" + }, + "oauth_client":{ + "client_id":"106014023", + "client_type":1 + }, + "app_info":{ + "app_id":"106014023", + "package_name":"deckers.thibault.aves" + }, + "configuration_version":"3.0", + "appInfos":[ + { + "package_name":"deckers.thibault.aves.profile", + "client":{ + "app_id":"106031461" + }, + "app_info":{ + "package_name":"deckers.thibault.aves.profile", + "app_id":"106031461" + }, + "oauth_client":{ + "client_type":1, + "client_id":"106031461" + } + }, + { + "package_name":"deckers.thibault.aves.debug", + "client":{ + "app_id":"106014297" + }, + "app_info":{ + "package_name":"deckers.thibault.aves.debug", + "app_id":"106014297" + }, + "oauth_client":{ + "client_type":1, + "client_id":"106014297" + } + }, + { + "package_name":"deckers.thibault.aves", + "client":{ + "app_id":"106014023" + }, + "app_info":{ + "package_name":"deckers.thibault.aves", + "app_id":"106014023" + }, + "oauth_client":{ + "client_type":1, + "client_id":"106014023" + } + } + ] +} \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 7b9253ca2..a8d4c0b0a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -81,6 +81,16 @@ android { // Google Play dimension "store" ext.useCrashlytics = true + ext.useHMS = false + // generate a universal APK without x86 native libs + ext.useNdkAbiFilters = true + } + + huawei { + // Huawei AppGallery + dimension "store" + ext.useCrashlytics = false + ext.useHMS = true // generate a universal APK without x86 native libs ext.useNdkAbiFilters = true } @@ -91,6 +101,7 @@ android { // cf https://android.izzysoft.de/articles/named/app-modules-2 dimension "store" ext.useCrashlytics = false + ext.useHMS = false // generate APK by ABI, but NDK ABI filters are incompatible with split APK generation ext.useNdkAbiFilters = false } @@ -129,6 +140,7 @@ android { lint { disable 'InvalidPackage' } + namespace 'deckers.thibault.aves' } flutter { @@ -147,12 +159,15 @@ dependencies { implementation 'androidx.multidex:multidex:2.0.1' implementation 'com.caverock:androidsvg-aar:1.4' implementation 'com.commonsware.cwac:document:0.4.1' - implementation 'com.drewnoakes:metadata-extractor:2.17.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.1' + implementation 'com.github.bumptech.glide:glide:4.13.2' + + // huawei flavor only + huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.5.2.300' kapt 'androidx.annotation:annotation:1.3.0' kapt 'com.github.bumptech.glide:compiler:4.13.0' @@ -163,8 +178,12 @@ dependencies { android.productFlavors.each { flavor -> def tasks = gradle.startParameter.taskRequests.toString().toLowerCase() if (tasks.contains(flavor.name) && flavor.ext.useCrashlytics) { - println("Building flavor with Crashlytics [${flavor.name}] - applying plugin") + println("Building flavor [${flavor.name}] with Crashlytics plugin") apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' } + if (tasks.contains(flavor.name) && flavor.ext.useHMS) { + println("Building flavor [${flavor.name}] with HMS plugin") + apply plugin: 'com.huawei.agconnect' + } } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index de4434e63..055de7299 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ + + + + + { @@ -246,10 +247,20 @@ class MainActivity : FlutterActivity() { } private fun pick(call: MethodCall) { - val pickedUri = call.argument("uri") - if (pickedUri != null) { + val pickedUris = call.argument>("uris") + if (pickedUris != null && pickedUris.isNotEmpty()) { + val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(context, Uri.parse(uriString)) } val intent = Intent().apply { - data = Uri.parse(pickedUri) + val firstUri = toUri(pickedUris.first()) + if (pickedUris.size == 1) { + data = firstUri + } else { + clipData = ClipData.newUri(contentResolver, null, firstUri).apply { + pickedUris.drop(1).forEach { + addItem(ClipData.Item(toUri(it))) + } + } + } addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } setResult(RESULT_OK, intent) @@ -307,6 +318,7 @@ class MainActivity : FlutterActivity() { const val INTENT_DATA_KEY_ACTION = "action" const val INTENT_DATA_KEY_FILTERS = "filters" const val INTENT_DATA_KEY_MIME_TYPE = "mimeType" + const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple" const val INTENT_DATA_KEY_PAGE = "page" const val INTENT_DATA_KEY_URI = "uri" const val INTENT_DATA_KEY_QUERY = "query" diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt index 195e9d4ab..9437745a3 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -216,7 +216,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { try { val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager if (clipboard != null) { - val clip = ClipData.newUri(context.contentResolver, label, getShareableUri(uri)) + val clip = ClipData.newUri(context.contentResolver, label, getShareableUri(context, uri)) clipboard.setPrimaryClip(clip) result.success(true) } else { @@ -239,7 +239,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val intent = Intent(Intent.ACTION_EDIT) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - .setDataAndType(getShareableUri(uri), mimeType) + .setDataAndType(getShareableUri(context, uri), mimeType) val started = safeStartActivityChooser(title, intent) result.success(started) @@ -256,7 +256,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val intent = Intent(Intent.ACTION_VIEW) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - .setDataAndType(getShareableUri(uri), mimeType) + .setDataAndType(getShareableUri(context, uri), mimeType) val started = safeStartActivityChooser(title, intent) result.success(started) @@ -286,7 +286,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val intent = Intent(Intent.ACTION_ATTACH_DATA) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - .setDataAndType(getShareableUri(uri), mimeType) + .setDataAndType(getShareableUri(context, uri), mimeType) val started = safeStartActivityChooser(title, intent) result.success(started) @@ -311,7 +311,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val intent = Intent(Intent.ACTION_SEND) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .setType(mimeType) - .putExtra(Intent.EXTRA_STREAM, getShareableUri(uri)) + .putExtra(Intent.EXTRA_STREAM, getShareableUri(context, uri)) safeStartActivityChooser(title, intent) } else { var mimeType = "*/*" @@ -368,18 +368,6 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { return false } - private fun getShareableUri(uri: Uri): Uri? { - return when (uri.scheme?.lowercase(Locale.ROOT)) { - ContentResolver.SCHEME_FILE -> { - uri.path?.let { path -> - val authority = "${context.applicationContext.packageName}.file_provider" - FileProvider.getUriForFile(context, authority, File(path)) - } - } - else -> uri - } - } - // shortcuts private fun pinShortcut(call: MethodCall, result: MethodChannel.Result) { @@ -443,5 +431,17 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { companion object { private val LOG_TAG = LogUtils.createTag() const val CHANNEL = "deckers.thibault/aves/app" + + fun getShareableUri(context: Context, uri: Uri): Uri? { + return when (uri.scheme?.lowercase(Locale.ROOT)) { + ContentResolver.SCHEME_FILE -> { + uri.path?.let { path -> + val authority = "${context.applicationContext.packageName}.file_provider" + FileProvider.getUriForFile(context, authority, File(path)) + } + } + else -> uri + } + } } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt index 0d2061740..84f6db94f 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt @@ -308,6 +308,8 @@ class DebugHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e) } } result.success(metadataMap) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt index be8ee35ee..7bb635ad6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt @@ -32,10 +32,6 @@ class DeviceHandler(private val context: Context) : MethodCallHandler { "canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context), "canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT), "canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP), - // as of google_maps_flutter v2.1.1, minSDK is 20 because of default PlatformView usage, - // but using hybrid composition would make it usable on API 19 too, - // cf https://github.com/flutter/flutter/issues/23728 - "canRenderGoogleMaps" to (sdkInt >= Build.VERSION_CODES.KITKAT_WATCH), "showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O), "supportEdgeToEdgeUIMode" to (sdkInt >= Build.VERSION_CODES.Q), ) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt index 3b7d3a897..f09bf8ce1 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt @@ -175,6 +175,8 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to extract file from XMP", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to extract file from XMP", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to extract file from XMP", e) } } result.error("extractXmpDataProp-empty", "failed to extract file from XMP uri=$uri prop=$dataPropPath", null) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt index a6ebfdfae..ce3d27742 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt @@ -331,6 +331,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } @@ -459,14 +461,14 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { // * `context.getContentResolver().getType()` sometimes returns an incorrect value // * `MediaMetadataRetriever.setDataSource()` sometimes fails with `status = 0x80000000` // * file extension is unreliable - // In the end, `metadata-extractor` is the most reliable, except for `tiff`/`dvd`/`mov` (false positives, false negatives), + // In the end, `metadata-extractor` is the most reliable, except for `tiff`/`dvd`/`mov`/`zip` (false positives, false negatives), // in which case we trust the file extension // cf https://github.com/drewnoakes/metadata-extractor/issues/296 if (path?.matches(TIFF_EXTENSION_PATTERN) == true) { metadataMap[KEY_MIME_TYPE] = MimeTypes.TIFF } else { dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) { - if (it != MimeTypes.TIFF && it != MimeTypes.DVD && it != MimeTypes.MOV) { + if (it != MimeTypes.TIFF && it != MimeTypes.DVD && it != MimeTypes.MOV && it != MimeTypes.ZIP) { metadataMap[KEY_MIME_TYPE] = it } } @@ -601,6 +603,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } @@ -727,6 +731,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } @@ -784,6 +790,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } result.error("getGeoTiffInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null) @@ -844,6 +852,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } result.error("getPanoramaInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null) @@ -894,7 +904,10 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { result.error("getXmp-exception", "failed to read XMP for mimeType=$mimeType uri=$uri", e.message) return } catch (e: NoClassDefFoundError) { - result.error("getXmp-error", "failed to read XMP for mimeType=$mimeType uri=$uri", e.message) + result.error("getXmp-noclass", "failed to read XMP for mimeType=$mimeType uri=$uri", e.message) + return + } catch (e: AssertionError) { + result.error("getXmp-assert", "failed to read XMP for mimeType=$mimeType uri=$uri", e.message) return } } @@ -1031,6 +1044,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt index a4796a3c6..a022bfd8d 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt @@ -77,7 +77,7 @@ class RegionFetcher internal constructor( } } if (newDecoder == null) { - result.error("getRegion-read-null", "failed to open file for uri=$uri regionRect=$regionRect", null) + result.error("getRegion-read-null", "failed to open file for mimeType=$mimeType uri=$uri regionRect=$regionRect", null) return } currentDecoderRef = LastDecoderRef(uri, newDecoder) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt index 241e50f02..e9a5dcabf 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt @@ -167,6 +167,8 @@ object MultiPage { Log.w(LOG_TAG, "failed to get motion photo offset from uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to get motion photo offset from uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to get motion photo offset from uri=$uri", e) } return null } 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 a355a8fa9..09406b448 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 @@ -204,6 +204,8 @@ class SourceEntry { // ignore } catch (e: NoClassDefFoundError) { // ignore + } catch (e: AssertionError) { + // ignore } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt index 13fa8dc2d..1781bba01 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt @@ -33,6 +33,8 @@ internal class ContentImageProvider : ImageProvider() { Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e) } catch (e: NoClassDefFoundError) { Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e) + } catch (e: AssertionError) { + Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e) } val mimeType = extractorMimeType ?: sourceMimeType diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index aa7801064..12ce8e4a5 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -1045,6 +1045,9 @@ abstract class ImageProvider { // used when skipping a move/creation op because the target file already exists val skippedFieldMap: HashMap = hashMapOf("skipped" to true) + // used when deleting instead of moving to bin because the target file no longer exists + val deletedFieldMap: HashMap = hashMapOf("deleted" to true) + fun isMediaUriPermissionGranted(context: Context, uri: Uri, mimeType: String): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val safeUri = StorageUtils.getMediaStoreScopedStorageSafeUri(uri, mimeType) 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 f6eeb0242..c8f38a340 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 @@ -443,18 +443,23 @@ class MediaStoreImageProvider : ImageProvider() { if (effectiveTargetDir != null) { val newFields = if (isCancelledOp()) skippedFieldMap else { val sourceFile = File(sourcePath) - moveSingle( - activity = activity, - sourceFile = sourceFile, - sourceUri = sourceUri, - targetDir = effectiveTargetDir, - targetDirDocFile = targetDirDocFile, - desiredName = desiredName ?: sourceFile.name, - nameConflictStrategy = nameConflictStrategy, - mimeType = mimeType, - copy = copy, - toBin = toBin, - ) + if (!sourceFile.exists() && toBin) { + delete(activity, sourceUri, sourcePath, mimeType = mimeType) + deletedFieldMap + } else { + moveSingle( + activity = activity, + sourceFile = sourceFile, + sourceUri = sourceUri, + targetDir = effectiveTargetDir, + targetDirDocFile = targetDirDocFile, + desiredName = desiredName ?: sourceFile.name, + nameConflictStrategy = nameConflictStrategy, + mimeType = mimeType, + copy = copy, + toBin = toBin, + ) + } } result["newFields"] = newFields result["success"] = true 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 fa810a2b4..c1a41b665 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 @@ -46,6 +46,7 @@ object MimeTypes { // vector const val SVG = "image/svg+xml" + // video private const val AVI = "video/avi" private const val AVI_VND = "video/vnd.avi" const val DVD = "video/dvd" @@ -57,6 +58,9 @@ object MimeTypes { private const val OGV = "video/ogg" private const val WEBM = "video/webm" + // others + const val ZIP = "application/zip" + fun isImage(mimeType: String?) = mimeType != null && mimeType.startsWith("image") fun isVideo(mimeType: String?) = mimeType != null && mimeType.startsWith("video") 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 9e5b51ced..8a0e3d2a0 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 @@ -21,7 +21,6 @@ import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.PermissionManager.getGrantedDirForPath import deckers.thibault.aves.utils.UriUtils.tryParseId import java.io.File -import java.io.FileNotFoundException import java.io.InputStream import java.io.OutputStream import java.util.* @@ -404,37 +403,37 @@ object StorageUtils { // returns the directory `DocumentFile` (from tree URI when scoped storage is required, `File` otherwise) // returns null if directory does not exist and could not be created fun createDirectoryDocIfAbsent(context: Context, dirPath: String): DocumentFileCompat? { - val cleanDirPath = ensureTrailingSeparator(dirPath) - return if (requireAccessPermission(context, cleanDirPath) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val grantedDir = getGrantedDirForPath(context, cleanDirPath) ?: return null - val rootTreeDocumentUri = convertDirPathToTreeDocumentUri(context, grantedDir) ?: return null - var parentFile: DocumentFileCompat? = DocumentFileCompat.fromTreeUri(context, rootTreeDocumentUri) ?: return null - val pathIterator = getPathStepIterator(context, cleanDirPath, grantedDir) - while (pathIterator?.hasNext() == true) { - val dirName = pathIterator.next() - var dirFile = findDocumentFileIgnoreCase(parentFile, dirName) - if (dirFile == null || !dirFile.exists()) { - try { + try { + val cleanDirPath = ensureTrailingSeparator(dirPath) + return if (requireAccessPermission(context, cleanDirPath) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val grantedDir = getGrantedDirForPath(context, cleanDirPath) ?: return null + val rootTreeDocumentUri = convertDirPathToTreeDocumentUri(context, grantedDir) ?: return null + var parentFile: DocumentFileCompat? = DocumentFileCompat.fromTreeUri(context, rootTreeDocumentUri) ?: return null + val pathIterator = getPathStepIterator(context, cleanDirPath, grantedDir) + while (pathIterator?.hasNext() == true) { + val dirName = pathIterator.next() + var dirFile = findDocumentFileIgnoreCase(parentFile, dirName) + if (dirFile == null || !dirFile.exists()) { dirFile = parentFile?.createDirectory(dirName) if (dirFile == null) { Log.e(LOG_TAG, "failed to create directory with name=$dirName from parent=$parentFile") return null } - } catch (e: FileNotFoundException) { - Log.e(LOG_TAG, "failed to create directory with name=$dirName from parent=$parentFile", e) - return null } + parentFile = dirFile } - parentFile = dirFile + parentFile + } else { + val directory = File(cleanDirPath) + if (!directory.exists() && !directory.mkdirs()) { + Log.e(LOG_TAG, "failed to create directories at path=$cleanDirPath") + return null + } + DocumentFileCompat.fromFile(directory) } - parentFile - } else { - val directory = File(cleanDirPath) - if (!directory.exists() && !directory.mkdirs()) { - Log.e(LOG_TAG, "failed to create directories at path=$cleanDirPath") - return null - } - DocumentFileCompat.fromFile(directory) + } catch (e: Exception) { + Log.e(LOG_TAG, "failed to create directory at path=$dirPath", e) + return null } } diff --git a/android/build.gradle b/android/build.gradle index ec015c9b9..52ec57d77 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,19 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.6.20' + ext.kotlin_version = '1.6.21' repositories { google() mavenCentral() + maven { url 'https://developer.huawei.com/repo/' } } dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' + classpath 'com.android.tools.build:gradle:7.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - // GMS & Firebase Crashlytics are not actually used by all flavors + // GMS & Firebase Crashlytics (used by some flavors only) classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' + // HMS (used by some flavors only) + classpath 'com.huawei.agconnect:agcp:1.5.2.300' } } @@ -18,6 +21,7 @@ allprojects { repositories { google() mavenCentral() + 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 6672c658d..080650cd5 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.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip diff --git a/fastlane/metadata/android/de/images/phoneScreenshots/1.png b/fastlane/metadata/android/de/images/phoneScreenshots/1.png index 149abe06e..a4feab64f 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/de/images/phoneScreenshots/2.png b/fastlane/metadata/android/de/images/phoneScreenshots/2.png index 1df869df0..bb7b53e99 100644 Binary files a/fastlane/metadata/android/de/images/phoneScreenshots/2.png and b/fastlane/metadata/android/de/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/de/images/phoneScreenshots/3.png b/fastlane/metadata/android/de/images/phoneScreenshots/3.png index b1dca7f8d..eb5b0abf2 100644 Binary files a/fastlane/metadata/android/de/images/phoneScreenshots/3.png and b/fastlane/metadata/android/de/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/de/images/phoneScreenshots/4.png b/fastlane/metadata/android/de/images/phoneScreenshots/4.png index 73b826187..7566991ad 100644 Binary files a/fastlane/metadata/android/de/images/phoneScreenshots/4.png and b/fastlane/metadata/android/de/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/de/images/phoneScreenshots/5.png b/fastlane/metadata/android/de/images/phoneScreenshots/5.png index 74bc802fa..0a6a56bb6 100644 Binary files a/fastlane/metadata/android/de/images/phoneScreenshots/5.png and b/fastlane/metadata/android/de/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/de/images/phoneScreenshots/6.png b/fastlane/metadata/android/de/images/phoneScreenshots/6.png index 56099d117..8d7fec3d3 100644 Binary files a/fastlane/metadata/android/de/images/phoneScreenshots/6.png and b/fastlane/metadata/android/de/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/en-US/changelogs/1071.txt b/fastlane/metadata/android/en-US/changelogs/1071.txt new file mode 100644 index 000000000..38d163888 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/1071.txt @@ -0,0 +1,5 @@ +In v1.6.5: +- bottom navigation bar +- fast scroll with breadcrumbs +- settings search +Full changelog available on GitHub diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index bf177de91..986afd0af 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/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png index cab24996a..699c677f4 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png index 96767e884..5893043b9 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png index 5aa80520d..727166076 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png index 763fab5f2..f5a0b693b 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png index f2e09ae08..dc41af368 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.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 4480d45e4..691254b62 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/es-MX/images/phoneScreenshots/2.png b/fastlane/metadata/android/es-MX/images/phoneScreenshots/2.png index 94170fd16..619af80e6 100644 Binary files a/fastlane/metadata/android/es-MX/images/phoneScreenshots/2.png and b/fastlane/metadata/android/es-MX/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/es-MX/images/phoneScreenshots/3.png b/fastlane/metadata/android/es-MX/images/phoneScreenshots/3.png index 7e6ed547e..a28e9bf7e 100644 Binary files a/fastlane/metadata/android/es-MX/images/phoneScreenshots/3.png and b/fastlane/metadata/android/es-MX/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/es-MX/images/phoneScreenshots/4.png b/fastlane/metadata/android/es-MX/images/phoneScreenshots/4.png index 6d3fdb80f..bdabfd3f4 100644 Binary files a/fastlane/metadata/android/es-MX/images/phoneScreenshots/4.png and b/fastlane/metadata/android/es-MX/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/es-MX/images/phoneScreenshots/5.png b/fastlane/metadata/android/es-MX/images/phoneScreenshots/5.png index 236a70926..303fe809e 100644 Binary files a/fastlane/metadata/android/es-MX/images/phoneScreenshots/5.png and b/fastlane/metadata/android/es-MX/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/es-MX/images/phoneScreenshots/6.png b/fastlane/metadata/android/es-MX/images/phoneScreenshots/6.png index cc9d9e252..7cbfb60ce 100644 Binary files a/fastlane/metadata/android/es-MX/images/phoneScreenshots/6.png and b/fastlane/metadata/android/es-MX/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/fr/images/phoneScreenshots/1.png b/fastlane/metadata/android/fr/images/phoneScreenshots/1.png index c139324b1..0fd4909e9 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/fr/images/phoneScreenshots/2.png b/fastlane/metadata/android/fr/images/phoneScreenshots/2.png index 47c0852ce..05cd6c007 100644 Binary files a/fastlane/metadata/android/fr/images/phoneScreenshots/2.png and b/fastlane/metadata/android/fr/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/fr/images/phoneScreenshots/3.png b/fastlane/metadata/android/fr/images/phoneScreenshots/3.png index d1d9df6a8..7a42d6be8 100644 Binary files a/fastlane/metadata/android/fr/images/phoneScreenshots/3.png and b/fastlane/metadata/android/fr/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/fr/images/phoneScreenshots/4.png b/fastlane/metadata/android/fr/images/phoneScreenshots/4.png index 792d081af..4d2f5116d 100644 Binary files a/fastlane/metadata/android/fr/images/phoneScreenshots/4.png and b/fastlane/metadata/android/fr/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/fr/images/phoneScreenshots/5.png b/fastlane/metadata/android/fr/images/phoneScreenshots/5.png index 1083c6be4..48dfa2352 100644 Binary files a/fastlane/metadata/android/fr/images/phoneScreenshots/5.png and b/fastlane/metadata/android/fr/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/fr/images/phoneScreenshots/6.png b/fastlane/metadata/android/fr/images/phoneScreenshots/6.png index 077def362..7597244d7 100644 Binary files a/fastlane/metadata/android/fr/images/phoneScreenshots/6.png and b/fastlane/metadata/android/fr/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/id/images/phoneScreenshots/1.png b/fastlane/metadata/android/id/images/phoneScreenshots/1.png index f157c0be5..2a05491b8 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/id/images/phoneScreenshots/2.png b/fastlane/metadata/android/id/images/phoneScreenshots/2.png index 25db2f7d4..386bba1a8 100644 Binary files a/fastlane/metadata/android/id/images/phoneScreenshots/2.png and b/fastlane/metadata/android/id/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/id/images/phoneScreenshots/3.png b/fastlane/metadata/android/id/images/phoneScreenshots/3.png index c5f809a92..8ef649c71 100644 Binary files a/fastlane/metadata/android/id/images/phoneScreenshots/3.png and b/fastlane/metadata/android/id/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/id/images/phoneScreenshots/4.png b/fastlane/metadata/android/id/images/phoneScreenshots/4.png index 022e506a5..f7e145bb2 100644 Binary files a/fastlane/metadata/android/id/images/phoneScreenshots/4.png and b/fastlane/metadata/android/id/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/id/images/phoneScreenshots/5.png b/fastlane/metadata/android/id/images/phoneScreenshots/5.png index 78286da77..a3ae51c2c 100644 Binary files a/fastlane/metadata/android/id/images/phoneScreenshots/5.png and b/fastlane/metadata/android/id/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/id/images/phoneScreenshots/6.png b/fastlane/metadata/android/id/images/phoneScreenshots/6.png index 75ba57bca..a973e9bcf 100644 Binary files a/fastlane/metadata/android/id/images/phoneScreenshots/6.png and b/fastlane/metadata/android/id/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/it/images/phoneScreenshots/1.png b/fastlane/metadata/android/it/images/phoneScreenshots/1.png index ceb69cddb..72c0e69d3 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/it/images/phoneScreenshots/2.png b/fastlane/metadata/android/it/images/phoneScreenshots/2.png index 408e48e72..9f291bbca 100644 Binary files a/fastlane/metadata/android/it/images/phoneScreenshots/2.png and b/fastlane/metadata/android/it/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/it/images/phoneScreenshots/3.png b/fastlane/metadata/android/it/images/phoneScreenshots/3.png index e88b88ec0..fb089dd6c 100644 Binary files a/fastlane/metadata/android/it/images/phoneScreenshots/3.png and b/fastlane/metadata/android/it/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/it/images/phoneScreenshots/4.png b/fastlane/metadata/android/it/images/phoneScreenshots/4.png index 91ce48feb..e49333640 100644 Binary files a/fastlane/metadata/android/it/images/phoneScreenshots/4.png and b/fastlane/metadata/android/it/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/it/images/phoneScreenshots/5.png b/fastlane/metadata/android/it/images/phoneScreenshots/5.png index bc7f0e12b..e8d6e3691 100644 Binary files a/fastlane/metadata/android/it/images/phoneScreenshots/5.png and b/fastlane/metadata/android/it/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/it/images/phoneScreenshots/6.png b/fastlane/metadata/android/it/images/phoneScreenshots/6.png index 620873843..0780544c6 100644 Binary files a/fastlane/metadata/android/it/images/phoneScreenshots/6.png and b/fastlane/metadata/android/it/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/ja/full_description.txt b/fastlane/metadata/android/ja/full_description.txt index 2b1431c48..f489d7765 100644 --- a/fastlane/metadata/android/ja/full_description.txt +++ b/fastlane/metadata/android/ja/full_description.txt @@ -1,7 +1,7 @@ -Avesはあらゆる画像や動画を扱うことができ、一般的なJPEGやMP4はもちろん、 マルチページTIFF、SVG、古いAVIなどの珍しい形式にも対応しています! +Avesはあらゆる画像や動画を扱うことができ、一般的なJPEGやMP4はもちろん、 マルチページTIFF、SVG、古いAVIなどの珍しい形式にも対応しています! -メディアコレクションをスキャンして、モーションフォトパノラマ(Photo Sphere)、360°動画GeoTIFFファイルなどを識別します。 +メディアコレクションをスキャンして、モーションフォトパノラマ(Photo Sphere)、360°動画GeoTIFFファイルなどを識別します。 -ナビゲーションと検索は、Avesの重要な部分です。アルバムから写真、タグ、地図などへ簡単に移動できます。 +ナビゲーションと検索は、Avesの重要な部分です。アルバムから写真、タグ、地図などへ簡単に移動できます。 Avesは、アプリショートカットグローバル検索などの機能を、Android(API 19から32まで、つまりAndroid 4.4から12 Lまで)と統合しています。また、メディアビューワーメディアピッカーとしても機能します。 \ No newline at end of file diff --git a/fastlane/metadata/android/ja/images/phoneScreenshots/1.png b/fastlane/metadata/android/ja/images/phoneScreenshots/1.png index b7f99f9c7..727e65483 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/ja/images/phoneScreenshots/2.png b/fastlane/metadata/android/ja/images/phoneScreenshots/2.png index 8b4364e04..225213380 100644 Binary files a/fastlane/metadata/android/ja/images/phoneScreenshots/2.png and b/fastlane/metadata/android/ja/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/ja/images/phoneScreenshots/3.png b/fastlane/metadata/android/ja/images/phoneScreenshots/3.png index e8b879179..4b85e913a 100644 Binary files a/fastlane/metadata/android/ja/images/phoneScreenshots/3.png and b/fastlane/metadata/android/ja/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/ja/images/phoneScreenshots/4.png b/fastlane/metadata/android/ja/images/phoneScreenshots/4.png index fd39d1ef1..b16fcd1f3 100644 Binary files a/fastlane/metadata/android/ja/images/phoneScreenshots/4.png and b/fastlane/metadata/android/ja/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/ja/images/phoneScreenshots/5.png b/fastlane/metadata/android/ja/images/phoneScreenshots/5.png index 39b6769ea..b57fa9ea2 100644 Binary files a/fastlane/metadata/android/ja/images/phoneScreenshots/5.png and b/fastlane/metadata/android/ja/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/ja/images/phoneScreenshots/6.png b/fastlane/metadata/android/ja/images/phoneScreenshots/6.png index d358bb072..682f00a67 100644 Binary files a/fastlane/metadata/android/ja/images/phoneScreenshots/6.png and b/fastlane/metadata/android/ja/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/ko/images/phoneScreenshots/1.png b/fastlane/metadata/android/ko/images/phoneScreenshots/1.png index ba194324a..941382121 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/ko/images/phoneScreenshots/2.png b/fastlane/metadata/android/ko/images/phoneScreenshots/2.png index a601e74d5..3f1647e3f 100644 Binary files a/fastlane/metadata/android/ko/images/phoneScreenshots/2.png and b/fastlane/metadata/android/ko/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/ko/images/phoneScreenshots/3.png b/fastlane/metadata/android/ko/images/phoneScreenshots/3.png index 8206e30b9..62b7c19be 100644 Binary files a/fastlane/metadata/android/ko/images/phoneScreenshots/3.png and b/fastlane/metadata/android/ko/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/ko/images/phoneScreenshots/4.png b/fastlane/metadata/android/ko/images/phoneScreenshots/4.png index a089a9252..c80b35c0f 100644 Binary files a/fastlane/metadata/android/ko/images/phoneScreenshots/4.png and b/fastlane/metadata/android/ko/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/ko/images/phoneScreenshots/5.png b/fastlane/metadata/android/ko/images/phoneScreenshots/5.png index 206888b33..f61817ea3 100644 Binary files a/fastlane/metadata/android/ko/images/phoneScreenshots/5.png and b/fastlane/metadata/android/ko/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/ko/images/phoneScreenshots/6.png b/fastlane/metadata/android/ko/images/phoneScreenshots/6.png index 5ad1622f3..e179b2c05 100644 Binary files a/fastlane/metadata/android/ko/images/phoneScreenshots/6.png and b/fastlane/metadata/android/ko/images/phoneScreenshots/6.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 560bcfa3a..d86705a78 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/pt-BR/images/phoneScreenshots/2.png b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/2.png index e8abe4129..e2fe95143 100644 Binary files a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/2.png and b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/3.png b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/3.png index b28935bfc..cd473cb8f 100644 Binary files a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/3.png and b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/4.png b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/4.png index 0e8c26458..833120355 100644 Binary files a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/4.png and b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/5.png b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/5.png index 172a48660..bebe66d01 100644 Binary files a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/5.png and b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/6.png b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/6.png index 372ddca85..6e25bb7af 100644 Binary files a/fastlane/metadata/android/pt-BR/images/phoneScreenshots/6.png and b/fastlane/metadata/android/pt-BR/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/ru/images/phoneScreenshots/1.png b/fastlane/metadata/android/ru/images/phoneScreenshots/1.png index 2facdbc61..e3aba1e4e 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/ru/images/phoneScreenshots/2.png b/fastlane/metadata/android/ru/images/phoneScreenshots/2.png index ca1770023..2758b8b4a 100644 Binary files a/fastlane/metadata/android/ru/images/phoneScreenshots/2.png and b/fastlane/metadata/android/ru/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/ru/images/phoneScreenshots/3.png b/fastlane/metadata/android/ru/images/phoneScreenshots/3.png index ac3b92614..f576406e9 100644 Binary files a/fastlane/metadata/android/ru/images/phoneScreenshots/3.png and b/fastlane/metadata/android/ru/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/ru/images/phoneScreenshots/4.png b/fastlane/metadata/android/ru/images/phoneScreenshots/4.png index 4a54f9f2e..b1a478dda 100644 Binary files a/fastlane/metadata/android/ru/images/phoneScreenshots/4.png and b/fastlane/metadata/android/ru/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/ru/images/phoneScreenshots/5.png b/fastlane/metadata/android/ru/images/phoneScreenshots/5.png index 38cb374c4..df6368d83 100644 Binary files a/fastlane/metadata/android/ru/images/phoneScreenshots/5.png and b/fastlane/metadata/android/ru/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/ru/images/phoneScreenshots/6.png b/fastlane/metadata/android/ru/images/phoneScreenshots/6.png index f14b4f9bc..e3454036d 100644 Binary files a/fastlane/metadata/android/ru/images/phoneScreenshots/6.png and b/fastlane/metadata/android/ru/images/phoneScreenshots/6.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 3553a0465..f762e1bbb 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/fastlane/metadata/android/zh-CN/images/phoneScreenshots/2.png b/fastlane/metadata/android/zh-CN/images/phoneScreenshots/2.png index 82ce94936..297d5e4af 100644 Binary files a/fastlane/metadata/android/zh-CN/images/phoneScreenshots/2.png and b/fastlane/metadata/android/zh-CN/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/zh-CN/images/phoneScreenshots/3.png b/fastlane/metadata/android/zh-CN/images/phoneScreenshots/3.png index c9c49f3ee..b367d768f 100644 Binary files a/fastlane/metadata/android/zh-CN/images/phoneScreenshots/3.png and b/fastlane/metadata/android/zh-CN/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/zh-CN/images/phoneScreenshots/4.png b/fastlane/metadata/android/zh-CN/images/phoneScreenshots/4.png index a5b897d83..ef4ce2758 100644 Binary files a/fastlane/metadata/android/zh-CN/images/phoneScreenshots/4.png and b/fastlane/metadata/android/zh-CN/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/zh-CN/images/phoneScreenshots/5.png b/fastlane/metadata/android/zh-CN/images/phoneScreenshots/5.png index e1942c08e..a29c7d376 100644 Binary files a/fastlane/metadata/android/zh-CN/images/phoneScreenshots/5.png and b/fastlane/metadata/android/zh-CN/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/zh-CN/images/phoneScreenshots/6.png b/fastlane/metadata/android/zh-CN/images/phoneScreenshots/6.png index 7e684f597..9dd4e80de 100644 Binary files a/fastlane/metadata/android/zh-CN/images/phoneScreenshots/6.png and b/fastlane/metadata/android/zh-CN/images/phoneScreenshots/6.png differ diff --git a/lib/app_flavor.dart b/lib/app_flavor.dart index 4dfcd54d6..6cd5520ae 100644 --- a/lib/app_flavor.dart +++ b/lib/app_flavor.dart @@ -1,5 +1,13 @@ -enum AppFlavor { play, izzy } +enum AppFlavor { play, huawei, izzy } extension ExtraAppFlavor on AppFlavor { - bool get canEnableErrorReporting => this == AppFlavor.play; + bool get canEnableErrorReporting { + switch (this) { + case AppFlavor.play: + return true; + case AppFlavor.huawei: + case AppFlavor.izzy: + return false; + } + } } diff --git a/lib/app_mode.dart b/lib/app_mode.dart index 34f38a48b..70ed305da 100644 --- a/lib/app_mode.dart +++ b/lib/app_mode.dart @@ -1,11 +1,13 @@ -enum AppMode { main, pickMediaExternal, pickMediaInternal, pickFilterInternal, view } +enum AppMode { main, pickSingleMediaExternal, pickMultipleMediaExternal, pickMediaInternal, pickFilterInternal, view } extension ExtraAppMode on AppMode { - bool get canSearch => this == AppMode.main || this == AppMode.pickMediaExternal; + bool get canSearch => this == AppMode.main || this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal; - bool get canSelect => this == AppMode.main; + bool get canSelectMedia => this == AppMode.main || this == AppMode.pickMultipleMediaExternal; - bool get hasDrawer => this == AppMode.main || this == AppMode.pickMediaExternal; + bool get canSelectFilter => this == AppMode.main; - bool get isPickingMedia => this == AppMode.pickMediaExternal || this == AppMode.pickMediaInternal; + bool get hasDrawer => this == AppMode.main || this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal; + + bool get isPickingMedia => this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal || this == AppMode.pickMediaInternal; } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index c6977369b..f2acda38c 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -124,6 +124,8 @@ "mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleTerrain": "Google Maps (Gelände)", + "mapStyleHuaweiNormal": "Petal Maps", + "mapStyleHuaweiTerrain": "Petal Maps (Gelände)", "mapStyleOsmHot": "Humanitäres OSM", "mapStyleStamenToner": "Stamen Toner (SchwarzWeiß)", "mapStyleStamenWatercolor": "Stamen Watercolor (Aquarell)", @@ -406,6 +408,8 @@ "settingsSystemDefault": "System", "settingsDefault": "Standard", + "settingsSearchFieldLabel": "Einstellungen durchsuchen", + "settingsSearchEmpty": "Keine passende Einstellung", "settingsActionExport": "Exportieren", "settingsActionImport": "Importieren", @@ -415,6 +419,7 @@ "settingsSectionNavigation": "Navigation", "settingsHome": "Startseite", + "settingsShowBottomNavigationBar": "Untere Navigationsleiste anzeigen", "settingsKeepScreenOnTile": "Bildschirm eingeschaltet lassen", "settingsKeepScreenOnTitle": "Bildschirm eingeschaltet lassen", "settingsDoubleBackExit": "Zum Verlassen zweimal „zurück“ tippen", @@ -434,7 +439,10 @@ "settingsNavigationDrawerAddAlbum": "Album hinzufügen", "settingsSectionThumbnails": "Vorschaubilder", + "settingsThumbnailOverlayTile": "Überlagerung", + "settingsThumbnailOverlayTitle": "Überlagerung", "settingsThumbnailShowFavouriteIcon": "Favoriten-Symbol anzeigen", + "settingsThumbnailShowTagIcon": "Tag-Symbol anzeigen", "settingsThumbnailShowLocationIcon": "Standort-Symbol anzeigen", "settingsThumbnailShowMotionPhotoIcon": "Bewegungsfoto-Symbol anzeigen", "settingsThumbnailShowRating": "Bewertung anzeigen", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 96359ab8d..0dc847cfa 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -164,6 +164,8 @@ "mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleTerrain": "Google Maps (Terrain)", + "mapStyleHuaweiNormal": "Petal Maps", + "mapStyleHuaweiTerrain": "Petal Maps (Terrain)", "mapStyleOsmHot": "Humanitarian OSM", "mapStyleStamenToner": "Stamen Toner", "mapStyleStamenWatercolor": "Stamen Watercolor", @@ -586,6 +588,8 @@ "settingsSystemDefault": "System", "settingsDefault": "Default", + "settingsSearchFieldLabel": "Search settings", + "settingsSearchEmpty": "No matching setting", "settingsActionExport": "Export", "settingsActionImport": "Import", @@ -595,6 +599,7 @@ "settingsSectionNavigation": "Navigation", "settingsHome": "Home", + "settingsShowBottomNavigationBar": "Show bottom navigation bar", "settingsKeepScreenOnTile": "Keep screen on", "settingsKeepScreenOnTitle": "Keep Screen On", "settingsDoubleBackExit": "Tap “back” twice to exit", @@ -614,7 +619,10 @@ "settingsNavigationDrawerAddAlbum": "Add album", "settingsSectionThumbnails": "Thumbnails", + "settingsThumbnailOverlayTile": "Overlay", + "settingsThumbnailOverlayTitle": "Overlay", "settingsThumbnailShowFavouriteIcon": "Show favorite icon", + "settingsThumbnailShowTagIcon": "Show tag icon", "settingsThumbnailShowLocationIcon": "Show location icon", "settingsThumbnailShowMotionPhotoIcon": "Show motion photo icon", "settingsThumbnailShowRating": "Show rating", @@ -751,7 +759,7 @@ "viewerInfoLabelUri": "URI", "viewerInfoLabelPath": "Path", "viewerInfoLabelDuration": "Duration", - "viewerInfoLabelOwner": "Owned by", + "viewerInfoLabelOwner": "Owner", "viewerInfoLabelCoordinates": "Coordinates", "viewerInfoLabelAddress": "Address", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 78ca559fc..d117a025f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -58,6 +58,9 @@ "entryActionPrint": "Imprimir", "entryActionShare": "Compartir", "entryActionViewSource": "Ver fuente", + "entryActionShowGeoTiffOnMap": "Mostrar como mapa superpuesto", + "entryActionConvertMotionPhotoToStillImage": "Convertir a imagen fija", + "entryActionViewMotionPhotoVideo": "Abrir video", "entryActionEdit": "Editar", "entryActionOpen": "Abrir con", "entryActionSetAs": "Establecer como", @@ -113,14 +116,16 @@ "videoLoopModeShortOnly": "Sólo videos cortos", "videoLoopModeAlways": "Siempre", + "videoControlsNone": "Ninguno", "videoControlsPlay": "Reproducir", "videoControlsPlaySeek": "Reproducir y buscar", "videoControlsPlayOutside": "Reproducir externamente", - "videoControlsNone": "Ninguno", - "mapStyleGoogleNormal": "Mapas de Google", - "mapStyleGoogleHybrid": "Mapas de Google (Híbrido)", - "mapStyleGoogleTerrain": "Mapas de Google (Superficie)", + "mapStyleGoogleNormal": "Google Maps", + "mapStyleGoogleHybrid": "Google Maps (Híbrido)", + "mapStyleGoogleTerrain": "Google Maps (Relieve)", + "mapStyleHuaweiNormal": "Petal Maps", + "mapStyleHuaweiTerrain": "Petal Maps (Relieve)", "mapStyleOsmHot": "OSM Humanitario", "mapStyleStamenToner": "Stamen Toner (Monocromático)", "mapStyleStamenWatercolor": "Stamen Watercolor (Acuarela)", @@ -184,6 +189,7 @@ "videoResumeButtonLabel": "REANUDAR", "setCoverDialogLatest": "Elemento más reciente", + "setCoverDialogAuto": "Automático", "setCoverDialogCustom": "Personalizado", "hideFilterConfirmationDialogMessage": "Fotos y videos que concuerden serán ocultados de su colección. Puede volver a mostrarlos desde los ajustes de «Privacidad».\n\n¿Está seguro de que desea ocultarlos?", @@ -237,6 +243,7 @@ "removeEntryMetadataDialogMore": "Más", "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP es necesario para reproducir la animación de una foto en movimiento.\n\n¿Está seguro de que desea removerlo?", + "convertMotionPhotoToStillImageWarningDialogMessage": "¿Está seguro?", "videoSpeedDialogLabel": "Velocidad de reproducción", @@ -264,6 +271,14 @@ "tileLayoutGrid": "Cuadrícula", "tileLayoutList": "Lista", + "coverDialogTabCover": "Carátula", + "coverDialogTabApp": "Aplicación", + "coverDialogTabColor": "Color", + + "appPickDialogTitle": "Escoger aplicación", + "appPickDialogNone": "Ninguna", + + "aboutPageTitle": "Acerca de", "aboutLinkSources": "Fuentes", "aboutLinkLicense": "Licencia", @@ -280,7 +295,8 @@ "aboutCredits": "Créditos", "aboutCreditsWorldAtlas1": "Esta aplicación usa un archivo TopoJSON de", "aboutCreditsWorldAtlas2": "bajo licencia ISC.", - "aboutCreditsTranslators": "Traductores", + "aboutCreditsTranslators": "Traductores:", + "aboutCreditsTranslatorLine": "{language}: {names}", "aboutLicenses": "Licencias de código abierto", "aboutLicensesBanner": "Esta aplicación usa los siguientes paquetes y librerías de código abierto.", @@ -394,6 +410,8 @@ "settingsSystemDefault": "Sistema", "settingsDefault": "Restablecer", + "settingsSearchFieldLabel": "Buscar ajustes", + "settingsSearchEmpty": "Sin coincidencias", "settingsActionExport": "Exportar", "settingsActionImport": "Importar", @@ -422,6 +440,8 @@ "settingsNavigationDrawerAddAlbum": "Agregar álbum", "settingsSectionThumbnails": "Miniaturas", + "settingsThumbnailOverlayTile": "Incrustaciones", + "settingsThumbnailOverlayTitle": "Incrustaciones", "settingsThumbnailShowFavouriteIcon": "Mostrar icono de favoritos", "settingsThumbnailShowLocationIcon": "Mostrar icono de ubicación", "settingsThumbnailShowMotionPhotoIcon": "Mostrar icono de foto en movimiento", @@ -456,7 +476,7 @@ "settingsViewerShowInformation": "Mostrar información", "settingsViewerShowInformationSubtitle": "Mostrar título, fecha, ubicación, etc.", "settingsViewerShowShootingDetails": "Mostrar detalles de toma", - "settingsViewerShowOverlayThumbnails": "Mostrar miniaturas", + "settingsViewerShowOverlayThumbnails": "Mostrar miniaturas", "settingsViewerEnableOverlayBlurEffect": "Efecto de difuminado", "settingsVideoPageTitle": "Ajustes de video", @@ -466,6 +486,8 @@ "settingsVideoEnableAutoPlay": "Reproducción automática", "settingsVideoLoopModeTile": "Modo bucle", "settingsVideoLoopModeTitle": "Modo bucle", + "settingsVideoQuickActionsTile": "Acciones rápidas para videos", + "settingsVideoQuickActionEditorTitle": "Acciones rápidas", "settingsSubtitleThemeTile": "Subtítulos", "settingsSubtitleThemeTitle": "Subtítulos", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 574a709b1..de2480249 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -124,6 +124,8 @@ "mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleHybrid": "Google Maps (Satellite)", "mapStyleGoogleTerrain": "Google Maps (Relief)", + "mapStyleHuaweiNormal": "Petal Maps", + "mapStyleHuaweiTerrain": "Petal Maps (Relief)", "mapStyleOsmHot": "OSM Humanitaire", "mapStyleStamenToner": "Stamen Toner (Monochrome)", "mapStyleStamenWatercolor": "Stamen Watercolor (Aquarelle)", @@ -406,6 +408,8 @@ "settingsSystemDefault": "Système", "settingsDefault": "Par défaut", + "settingsSearchFieldLabel": "Recherche de réglages", + "settingsSearchEmpty": "Aucun réglage correspondant", "settingsActionExport": "Exporter", "settingsActionImport": "Importer", @@ -415,6 +419,7 @@ "settingsSectionNavigation": "Navigation", "settingsHome": "Page d’accueil", + "settingsShowBottomNavigationBar": "Afficher la barre de navigation", "settingsKeepScreenOnTile": "Maintenir l’écran allumé", "settingsKeepScreenOnTitle": "Allumage de l’écran", "settingsDoubleBackExit": "Presser «\u00A0retour\u00A0» 2 fois pour quitter", @@ -434,7 +439,10 @@ "settingsNavigationDrawerAddAlbum": "Ajouter un album", "settingsSectionThumbnails": "Vignettes", + "settingsThumbnailOverlayTile": "Incrustations", + "settingsThumbnailOverlayTitle": "Incrustations", "settingsThumbnailShowFavouriteIcon": "Afficher l’icône de favori", + "settingsThumbnailShowTagIcon": "Afficher l’icône de libellé", "settingsThumbnailShowLocationIcon": "Afficher l’icône de lieu", "settingsThumbnailShowMotionPhotoIcon": "Afficher l’icône de photo animée", "settingsThumbnailShowRating": "Afficher la notation", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 0547da17c..b94a56778 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -17,7 +17,7 @@ "hideButtonLabel": "SEMBUNYIKAN", "continueButtonLabel": "SELANJUTNYA", - "cancelTooltip": "Batalkan", + "cancelTooltip": "Batal", "changeTooltip": "Ganti", "clearTooltip": "Hapus", "previousTooltip": "Sebelumnya", @@ -82,7 +82,7 @@ "entryInfoActionEditDate": "Ubah tanggal & waktu", "entryInfoActionEditLocation": "Ubah lokasi", - "entryInfoActionEditRating": "Ubah peringkat", + "entryInfoActionEditRating": "Ubah nilai", "entryInfoActionEditTags": "Ubah label", "entryInfoActionRemoveMetadata": "Hapus metadata", @@ -90,7 +90,7 @@ "filterFavouriteLabel": "Favorit", "filterLocationEmptyLabel": "Lokasi yang tidak ditemukan", "filterTagEmptyLabel": "Tidak dilabel", - "filterRatingUnratedLabel": "Belum diberi peringkat", + "filterRatingUnratedLabel": "Belum diberi nilai", "filterRatingRejectedLabel": "Ditolak", "filterTypeAnimatedLabel": "Teranimasi", "filterTypeMotionPhotoLabel": "Foto bergerak", @@ -124,6 +124,8 @@ "mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleTerrain": "Google Maps (Terrain)", + "mapStyleHuaweiNormal": "Petal Maps", + "mapStyleHuaweiTerrain": "Petal Maps (Terrain)", "mapStyleOsmHot": "Humanitarian OSM", "mapStyleStamenToner": "Stamen Toner", "mapStyleStamenWatercolor": "Stamen Watercolor", @@ -148,7 +150,7 @@ "albumTierNew": "Baru", "albumTierPinned": "Disemat", - "albumTierSpecial": "Umum", + "albumTierSpecial": "Biasa", "albumTierApps": "Aplikasi", "albumTierRegular": "Lainnya", @@ -235,7 +237,7 @@ "locationPickerUseThisLocationButton": "Gunakan lokasi ini", - "editEntryRatingDialogTitle": "Peringkat", + "editEntryRatingDialogTitle": "Nilai", "removeEntryMetadataDialogTitle": "Penghapusan Metadata", "removeEntryMetadataDialogMore": "Lebih Banyak", @@ -247,7 +249,7 @@ "videoStreamSelectionDialogVideo": "Video", "videoStreamSelectionDialogAudio": "Audio", - "videoStreamSelectionDialogText": "Subtitle", + "videoStreamSelectionDialogText": "Subjudul", "videoStreamSelectionDialogOff": "Mati", "videoStreamSelectionDialogTrack": "Trek", "videoStreamSelectionDialogNoSelection": "Tidak ada Trek yang lain.", @@ -322,7 +324,7 @@ "collectionSortDate": "Lewat tanggal", "collectionSortSize": "Lewat ukuran", "collectionSortName": "Lewat nama album & file", - "collectionSortRating": "Lewat peringkat", + "collectionSortRating": "Lewat nilai", "collectionGroupAlbum": "Lewat album", "collectionGroupMonth": "Lewat bulan", @@ -400,12 +402,14 @@ "searchSectionCountries": "Negara", "searchSectionPlaces": "Tempat", "searchSectionTags": "Label", - "searchSectionRating": "Peringkat", + "searchSectionRating": "Nilai", "settingsPageTitle": "Pengaturan", "settingsSystemDefault": "Sistem", "settingsDefault": "Default", + "settingsSearchFieldLabel": "Cari peraturan", + "settingsSearchEmpty": "Tidak ada peraturan yang cocok", "settingsActionExport": "Ekspor", "settingsActionImport": "Impor", @@ -415,6 +419,7 @@ "settingsSectionNavigation": "Navigasi", "settingsHome": "Beranda", + "settingsShowBottomNavigationBar": "Tampilkan bilah navigasi bawah", "settingsKeepScreenOnTile": "Biarkan layarnya menyala", "settingsKeepScreenOnTitle": "Biarkan Layarnya Menyala", "settingsDoubleBackExit": "Ketuk “kembali” dua kali untuk keluar", @@ -434,10 +439,13 @@ "settingsNavigationDrawerAddAlbum": "Tambahkan album", "settingsSectionThumbnails": "Thumbnail", + "settingsThumbnailOverlayTile": "Hamparan", + "settingsThumbnailOverlayTitle": "Hamparan", "settingsThumbnailShowFavouriteIcon": "Tampilkan ikon favorit", + "settingsThumbnailShowTagIcon": "Tampilkan ikon label", "settingsThumbnailShowLocationIcon": "Tampilkan ikon lokasi", "settingsThumbnailShowMotionPhotoIcon": "Tampilkan ikon Foto bergerak", - "settingsThumbnailShowRating": "Tampilkan peringkat", + "settingsThumbnailShowRating": "Tampilkan nilai", "settingsThumbnailShowRawIcon": "Tampilkan ikon raw", "settingsThumbnailShowVideoDuration": "Tampilkan durasi video", @@ -479,8 +487,8 @@ "settingsVideoLoopModeTile": "Putar ulang", "settingsVideoLoopModeTitle": "Putar Ulang", - "settingsSubtitleThemeTile": "Subtitle", - "settingsSubtitleThemeTitle": "Subtitle", + "settingsSubtitleThemeTile": "Subjudul", + "settingsSubtitleThemeTitle": "Subjudul", "settingsSubtitleThemeSample": "Ini adalah sampel.", "settingsSubtitleThemeTextAlignmentTile": "Perataan teks", "settingsSubtitleThemeTextAlignmentTitle": "Perataan Teks", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 87a7d1eb4..756339ddd 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -41,7 +41,7 @@ "chipActionGoToTagPage": "Mostra nelle etichette", "chipActionHide": "Nascondi", "chipActionPin": "Fissa in alto", - "chipActionUnpin": "Rimuovi dall'alto", + "chipActionUnpin": "Rimuovi dall’alto", "chipActionRename": "Rinomina", "chipActionSetCover": "Imposta copertina", "chipActionCreateAlbum": "Crea album", @@ -124,6 +124,8 @@ "mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleHybrid": "Google Maps (Ibrido)", "mapStyleGoogleTerrain": "Google Maps (Terreno)", + "mapStyleHuaweiNormal": "Petal Maps", + "mapStyleHuaweiTerrain": "Petal Maps (Terreno)", "mapStyleOsmHot": "OSM umanitario", "mapStyleStamenToner": "Stamen Toner (Monocromatico)", "mapStyleStamenWatercolor": "Stamen Watercolor (Acquerello)", @@ -193,7 +195,7 @@ "hideFilterConfirmationDialogMessage": "Le foto e i video corrispondenti saranno nascosti dalla tua collezione. Puoi mostrarli di nuovo dalle impostazioni della «Privacy». Sei sicuro di volerli nascondere?", "newAlbumDialogTitle": "Nuovo album", - "newAlbumDialogNameLabel": "Nome dell'album", + "newAlbumDialogNameLabel": "Nome dell’album", "newAlbumDialogNameLabelAlreadyExistsHelper": "La cartella esiste già", "newAlbumDialogStorageLabel": "Archiviazione:", @@ -219,7 +221,7 @@ "editEntryDateDialogTitle": "Data e ora", "editEntryDateDialogSetCustom": "Imposta data personalizzata", - "editEntryDateDialogCopyField": "Copia da un'altra data", + "editEntryDateDialogCopyField": "Copia da un’altra data", "editEntryDateDialogCopyItem": "Copia da un altro elemento", "editEntryDateDialogExtractFromTitle": "Estrai dal titolo", "editEntryDateDialogShift": "Turno", @@ -240,7 +242,7 @@ "removeEntryMetadataDialogTitle": "Rimozione dei metadati", "removeEntryMetadataDialogMore": "Altro", - "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP è richiesto per riprodurre il video all'interno di una foto in movimento.\n\nSei sicuro di volerlo rimuovere?", + "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP è richiesto per riprodurre il video all’interno di una foto in movimento.\n\nSei sicuro di volerlo rimuovere?", "convertMotionPhotoToStillImageWarningDialogMessage": "Sei sicuro?", "videoSpeedDialogLabel": "Velocità di riproduzione", @@ -282,7 +284,7 @@ "aboutLinkPolicy": "Informativa sulla privacy", "aboutBug": "Segnalazione bug", - "aboutBugSaveLogInstruction": "Salva i log dell'app in un file", + "aboutBugSaveLogInstruction": "Salva i log dell’app in un file", "aboutBugSaveLogButton": "Salva", "aboutBugCopyInfoInstruction": "Copia le informazioni di sistema", "aboutBugCopyInfoButton": "Copia", @@ -312,8 +314,8 @@ "collectionActionHideTitleSearch": "Nascondi filtro", "collectionActionAddShortcut": "Aggiungi collegamento", "collectionActionEmptyBin": "Svuota cestino", - "collectionActionCopy": "Copia nell'album", - "collectionActionMove": "Sposta nell'album", + "collectionActionCopy": "Copia nell’album", + "collectionActionMove": "Sposta nell’album", "collectionActionRescan": "Riscansiona", "collectionActionEdit": "Modifica", @@ -374,8 +376,8 @@ "albumPickPageTitleMove": "Sposta", "albumPickPageTitlePick": "Seleziona", - "albumCamera": "Camera", - "albumDownload": "Download", + "albumCamera": "Fotocamera", + "albumDownload": "Scaricati", "albumScreenshots": "Screenshot", "albumScreenRecordings": "Registrazioni schermo", "albumVideoCaptures": "Scatti nei video", @@ -406,6 +408,8 @@ "settingsSystemDefault": "Sistema", "settingsDefault": "Predefinite", + "settingsSearchFieldLabel": "Ricerca impostazioni", + "settingsSearchEmpty": "Nessuna impostazione corrispondente", "settingsActionExport": "Esporta", "settingsActionImport": "Importa", @@ -415,6 +419,7 @@ "settingsSectionNavigation": "Navigazione", "settingsHome": "Pagina iniziale", + "settingsShowBottomNavigationBar": "Mostra la barra di navigazione in basso", "settingsKeepScreenOnTile": "Mantieni acceso lo schermo", "settingsKeepScreenOnTitle": "Illuminazione schermo", "settingsDoubleBackExit": "Tocca «indietro» due volte per uscire", @@ -434,11 +439,14 @@ "settingsNavigationDrawerAddAlbum": "Aggiungi album", "settingsSectionThumbnails": "Miniature", - "settingsThumbnailShowFavouriteIcon": "Mostra icona preferita", - "settingsThumbnailShowLocationIcon": "Mostra l'icona della posizione", - "settingsThumbnailShowMotionPhotoIcon": "Mostra l'icona della foto in movimento", + "settingsThumbnailOverlayTile": "Sovrapposizione", + "settingsThumbnailOverlayTitle": "Sovrapposizione", + "settingsThumbnailShowFavouriteIcon": "Mostra l’icona Preferiti", + "settingsThumbnailShowTagIcon": "Mostra l’icona Etichetta", + "settingsThumbnailShowLocationIcon": "Mostra l’icona Posizione", + "settingsThumbnailShowMotionPhotoIcon": "Mostra l’icona Foto in movimento", "settingsThumbnailShowRating": "Mostra la valutazione", - "settingsThumbnailShowRawIcon": "Mostra icona raw", + "settingsThumbnailShowRawIcon": "Mostra icona Raw", "settingsThumbnailShowVideoDuration": "Mostra la durata del video", "settingsCollectionQuickActionsTile": "Azioni rapide", @@ -463,7 +471,7 @@ "settingsViewerOverlayTile": "Sovrapposizione", "settingsViewerOverlayTitle": "Sovrapposizione", - "settingsViewerShowOverlayOnOpening": "Mostra all'apertura", + "settingsViewerShowOverlayOnOpening": "Mostra all’apertura", "settingsViewerShowMinimap": "Mostra la minimappa", "settingsViewerShowInformation": "Mostra informazioni", "settingsViewerShowInformationSubtitle": "Mostra titolo, data, posizione, ecc.", @@ -502,7 +510,7 @@ "settingsVideoGestureSideDoubleTapSeek": "Doppio tocco sui bordi dello schermo per cercare avanti/indietro", "settingsSectionPrivacy": "Privacy", - "settingsAllowInstalledAppAccess": "Consentire l'accesso all'inventario delle app", + "settingsAllowInstalledAppAccess": "Consentire l’accesso all’inventario delle app", "settingsAllowInstalledAppAccessSubtitle": "Utilizzato per migliorare la visualizzazione degli album", "settingsAllowErrorReporting": "Consenti segnalazione anonima degli errori", "settingsSaveSearchHistory": "Salva la cronologia delle ricerche", @@ -572,15 +580,15 @@ "mapStyleTitle": "Stile Mappa", "mapStyleTooltip": "Seleziona lo stile della mappa", - "mapZoomInTooltip": "Zoom in", - "mapZoomOutTooltip": "Zoom out", - "mapPointNorthUpTooltip": "Punta a nord verso l'alto", + "mapZoomInTooltip": "Ingrandisci", + "mapZoomOutTooltip": "Riduci", + "mapPointNorthUpTooltip": "Punta a nord verso l’alto", "mapAttributionOsmHot": "Dati della mappa © collaboratori di [OpenStreetMap](https://www.openstreetmap.org/copyright) - Titoli di [HOT](https://www.hotosm.org/) - Ospitato da [OSM France](https://openstreetmap.fr/)", "mapAttributionStamen": "Dati della mappa © collaboratori di [OpenStreetMap](https://www.openstreetmap.org/copyright) - Titoli di [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0)", "openMapPageTooltip": "Visualizza sulla pagina della mappa", "mapEmptyRegion": "Nessuna immagine in questa regione", - "viewerInfoOpenEmbeddedFailureFeedback": "Fallita l'estrazione dei dati incorporati", + "viewerInfoOpenEmbeddedFailureFeedback": "Fallita l’estrazione dei dati incorporati", "viewerInfoOpenLinkText": "Apri", "viewerInfoViewXmlLinkText": "Visualizza XML", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index cebf8e16a..268aa39e9 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -58,6 +58,9 @@ "entryActionPrint": "印刷", "entryActionShare": "共有", "entryActionViewSource": "ソースを表示", + "entryActionShowGeoTiffOnMap": "地図のオーバーレイとして表示", + "entryActionConvertMotionPhotoToStillImage": "静止画に変換", + "entryActionViewMotionPhotoVideo": "動画を開く", "entryActionEdit": "編集", "entryActionOpen": "アプリで開く", "entryActionSetAs": "登録", @@ -88,7 +91,7 @@ "filterLocationEmptyLabel": "位置情報なし", "filterTagEmptyLabel": "タグ情報なし", "filterRatingUnratedLabel": "評価情報なし", - "filterRatingRejectedLabel": "拒否されました", + "filterRatingRejectedLabel": "拒否", "filterTypeAnimatedLabel": "アニメーション", "filterTypeMotionPhotoLabel": "モーションフォト", "filterTypePanoramaLabel": "パノラマ", @@ -121,6 +124,8 @@ "mapStyleGoogleNormal": "Google マップ", "mapStyleGoogleHybrid": "Google マップ(ハイブリッド)", "mapStyleGoogleTerrain": "Google マップ(地形)", + "mapStyleHuaweiNormal": "Petal マップ", + "mapStyleHuaweiTerrain": "Petal マップ(地形)", "mapStyleOsmHot": "Humanitarian OSM", "mapStyleStamenToner": "Stamen Toner", "mapStyleStamenWatercolor": "Stamen Watercolor", @@ -184,6 +189,7 @@ "videoResumeButtonLabel": "再開", "setCoverDialogLatest": "最新のアイテム", + "setCoverDialogAuto": "自動", "setCoverDialogCustom": "カスタム", "hideFilterConfirmationDialogMessage": "一致する写真と動画はコレクションに表示されなくなります。「プライバシー」設定からいつでもアイテムを表示できます。\n\nこれらのアイテムを非表示にしますか?", @@ -237,6 +243,7 @@ "removeEntryMetadataDialogMore": "もっと見る", "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "モーションフォト内の動画を再生するには XMP が必要です。\n\n削除しますか?", + "convertMotionPhotoToStillImageWarningDialogMessage": "適用しますか?", "videoSpeedDialogLabel": "再生速度", @@ -264,6 +271,13 @@ "tileLayoutGrid": "グリッド表示", "tileLayoutList": "リスト表示", + "coverDialogTabCover": "カバー", + "coverDialogTabApp": "アプリ", + "coverDialogTabColor": "カラー", + + "appPickDialogTitle": "アプリを選ぶ", + "appPickDialogNone": "なし", + "aboutPageTitle": "アプリについて", "aboutLinkSources": "ソース", "aboutLinkLicense": "ライセンス", @@ -394,6 +408,8 @@ "settingsSystemDefault": "システム", "settingsDefault": "デフォルト", + "settingsSearchFieldLabel": "検索設定", + "settingsSearchEmpty": "一致する設定なし", "settingsActionExport": "エクスポート", "settingsActionImport": "インポート", @@ -403,6 +419,7 @@ "settingsSectionNavigation": "ナビゲーション", "settingsHome": "ホーム", + "settingsShowBottomNavigationBar": "下部のナビゲーションバーを表示", "settingsKeepScreenOnTile": "画面をオンのままにする", "settingsKeepScreenOnTitle": "画面をオンのままにする", "settingsDoubleBackExit": "「戻る」を2回タップして終了", @@ -422,7 +439,10 @@ "settingsNavigationDrawerAddAlbum": "アルバムを追加", "settingsSectionThumbnails": "サムネイル", + "settingsThumbnailOverlayTile": "オーバーレイ", + "settingsThumbnailOverlayTitle": "オーバーレイ", "settingsThumbnailShowFavouriteIcon": "お気に入りアイコンを表示", + "settingsThumbnailShowTagIcon": "タグアイコンを表示", "settingsThumbnailShowLocationIcon": "位置情報アイコンを表示", "settingsThumbnailShowMotionPhotoIcon": "モーションフォトアイコンを表示", "settingsThumbnailShowRating": "評価情報を表示", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 6487b8352..57658eff1 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -121,9 +121,11 @@ "videoControlsPlayOutside": "다른 앱에서 열기", "videoControlsNone": "없음", - "mapStyleGoogleNormal": "구글 지도", - "mapStyleGoogleHybrid": "구글 지도 (위성)", - "mapStyleGoogleTerrain": "구글 지도 (지형)", + "mapStyleGoogleNormal": "Google 지도", + "mapStyleGoogleHybrid": "Google 지도 (위성)", + "mapStyleGoogleTerrain": "Google 지도 (지형)", + "mapStyleHuaweiNormal": "Petal 지도", + "mapStyleHuaweiTerrain": "Petal 지도 (지형)", "mapStyleOsmHot": "Humanitarian OSM", "mapStyleStamenToner": "Stamen Toner (토너)", "mapStyleStamenWatercolor": "Stamen Watercolor (수채화)", @@ -406,6 +408,8 @@ "settingsSystemDefault": "시스템", "settingsDefault": "기본", + "settingsSearchFieldLabel": "설정 검색", + "settingsSearchEmpty": "결과가 없습니다", "settingsActionExport": "내보내기", "settingsActionImport": "가져오기", @@ -415,6 +419,7 @@ "settingsSectionNavigation": "탐색", "settingsHome": "홈", + "settingsShowBottomNavigationBar": "하단 탐색 모음 표시", "settingsKeepScreenOnTile": "화면 자동 꺼짐 방지", "settingsKeepScreenOnTitle": "화면 자동 꺼짐 방지", "settingsDoubleBackExit": "뒤로가기 두번 눌러 앱 종료하기", @@ -434,7 +439,10 @@ "settingsNavigationDrawerAddAlbum": "앨범 추가", "settingsSectionThumbnails": "섬네일", + "settingsThumbnailOverlayTile": "오버레이", + "settingsThumbnailOverlayTitle": "오버레이", "settingsThumbnailShowFavouriteIcon": "즐겨찾기 아이콘 표시", + "settingsThumbnailShowTagIcon": "태그 아이콘 표시", "settingsThumbnailShowLocationIcon": "위치 아이콘 표시", "settingsThumbnailShowMotionPhotoIcon": "모션 사진 아이콘 표시", "settingsThumbnailShowRating": "별점 표시", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index abe1d5345..0bae2a926 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -124,6 +124,8 @@ "mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleHybrid": "Google Maps (Híbrido)", "mapStyleGoogleTerrain": "Google Maps (Terreno)", + "mapStyleHuaweiNormal": "Petal Maps", + "mapStyleHuaweiTerrain": "Petal Maps (Terreno)", "mapStyleOsmHot": "OSM Humanitário", "mapStyleStamenToner": "Stamen Toner (Monocromático)", "mapStyleStamenWatercolor": "Stamen Watercolor (Aquarela)", @@ -406,6 +408,8 @@ "settingsSystemDefault": "Sistema", "settingsDefault": "Padrão", + "settingsSearchFieldLabel": "Pesquisar configuração", + "settingsSearchEmpty": "Nenhuma configuração correspondente", "settingsActionExport": "Exportar", "settingsActionImport": "Importar", @@ -415,6 +419,7 @@ "settingsSectionNavigation": "Navegação", "settingsHome": "Início", + "settingsShowBottomNavigationBar": "Mostrar barra de navegação inferior", "settingsKeepScreenOnTile": "Manter a tela ligada", "settingsKeepScreenOnTitle": "Manter a tela ligada", "settingsDoubleBackExit": "Toque em “voltar” duas vezes para sair", @@ -434,7 +439,10 @@ "settingsNavigationDrawerAddAlbum": "Adicionar álbum", "settingsSectionThumbnails": "Miniaturas", + "settingsThumbnailOverlayTile": "Sobreposição", + "settingsThumbnailOverlayTitle": "Sobreposição", "settingsThumbnailShowFavouriteIcon": "Mostrar ícone favorito", + "settingsThumbnailShowTagIcon": "Mostrar ícone de etiqueta", "settingsThumbnailShowLocationIcon": "Mostrar ícone de localização", "settingsThumbnailShowMotionPhotoIcon": "Mostrar ícone de foto em movimento", "settingsThumbnailShowRating": "Mostrar classificação", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 5f3e9aee7..024d7b1ea 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -124,6 +124,8 @@ "mapStyleGoogleNormal": "Google Карты", "mapStyleGoogleHybrid": "Google Карты (Гибридный)", "mapStyleGoogleTerrain": "Google Карты (Местность)", + "mapStyleHuaweiNormal": "Petal Карты", + "mapStyleHuaweiTerrain": "Petal Карты (Местность)", "mapStyleOsmHot": "Команда гуманитарной картопомощи", "mapStyleStamenToner": "Stamen Toner", "mapStyleStamenWatercolor": "Stamen Watercolor", @@ -434,6 +436,8 @@ "settingsNavigationDrawerAddAlbum": "Добавить альбом", "settingsSectionThumbnails": "Эскизы", + "settingsThumbnailOverlayTile": "Наложение", + "settingsThumbnailOverlayTitle": "Наложение", "settingsThumbnailShowFavouriteIcon": "Показать значок избранного", "settingsThumbnailShowLocationIcon": "Показать значок местоположения", "settingsThumbnailShowMotionPhotoIcon": "Показать значок «живого фото»", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f08d3ad1f..68503af3f 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -121,9 +121,11 @@ "videoControlsPlayOutside": "用其他播放器打开", "videoControlsNone": "无", - "mapStyleGoogleNormal": "Google Maps", - "mapStyleGoogleHybrid": "Google Maps (Hybrid)", - "mapStyleGoogleTerrain": "Google Maps (Terrain)", + "mapStyleGoogleNormal": "Google 地图", + "mapStyleGoogleHybrid": "Google 地图 (卫星图像)", + "mapStyleGoogleTerrain": "Google 地图 (地形)", + "mapStyleHuaweiNormal": "Petal 地图", + "mapStyleHuaweiTerrain": "Petal 地图 (地形)", "mapStyleOsmHot": "Humanitarian OSM", "mapStyleStamenToner": "Stamen Toner", "mapStyleStamenWatercolor": "Stamen Watercolor", @@ -406,6 +408,8 @@ "settingsSystemDefault": "系统", "settingsDefault": "默认", + "settingsSearchFieldLabel": "搜索设置", + "settingsSearchEmpty": "无匹配设置项", "settingsActionExport": "导出", "settingsActionImport": "导入", @@ -415,6 +419,7 @@ "settingsSectionNavigation": "导航", "settingsHome": "主页", + "settingsShowBottomNavigationBar": "显示底部导航栏", "settingsKeepScreenOnTile": "保持亮屏", "settingsKeepScreenOnTitle": "保持亮屏", "settingsDoubleBackExit": "按两次返回键退出", @@ -434,7 +439,10 @@ "settingsNavigationDrawerAddAlbum": "添加相册", "settingsSectionThumbnails": "缩略图", + "settingsThumbnailOverlayTile": "叠加层", + "settingsThumbnailOverlayTitle": "叠加层", "settingsThumbnailShowFavouriteIcon": "显示收藏图标", + "settingsThumbnailShowTagIcon": "显示标签图标", "settingsThumbnailShowLocationIcon": "显示位置图标", "settingsThumbnailShowMotionPhotoIcon": "显示动态照片图标", "settingsThumbnailShowRating": "显示评分", diff --git a/lib/main_huawei.dart b/lib/main_huawei.dart new file mode 100644 index 000000000..6475eb310 --- /dev/null +++ b/lib/main_huawei.dart @@ -0,0 +1,6 @@ +import 'package:aves/app_flavor.dart'; +import 'package:aves/main_common.dart'; + +void main() { + mainCommon(AppFlavor.huawei); +} diff --git a/lib/model/availability.dart b/lib/model/availability.dart index bdbb691fd..fb5446d6e 100644 --- a/lib/model/availability.dart +++ b/lib/model/availability.dart @@ -1,22 +1,21 @@ -import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/enums/map_style.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart'; -import 'package:google_api_availability/google_api_availability.dart'; abstract class AvesAvailability { void onResume(); Future get isConnected; - Future get hasPlayServices; - Future get canLocatePlaces; - Future get canUseGoogleMaps; + List get mapStyles; } class LiveAvesAvailability implements AvesAvailability { - bool? _isConnected, _hasPlayServices; + bool? _isConnected; LiveAvesAvailability() { Connectivity().onConnectivityChanged.listen(_updateConnectivityFromResult); @@ -41,19 +40,14 @@ class LiveAvesAvailability implements AvesAvailability { } } + // local geocoding with `geocoder` seems to require Google Play Services + // what about devices with Huawei Mobile Services? @override - Future get hasPlayServices async { - if (_hasPlayServices != null) return SynchronousFuture(_hasPlayServices!); - final result = await GoogleApiAvailability.instance.checkGooglePlayServicesAvailability(); - _hasPlayServices = result == GooglePlayServicesAvailability.success; - debugPrint('Device has Play Services=$_hasPlayServices'); - return _hasPlayServices!; - } - - // local geocoding with `geocoder` requires Play Services - @override - Future get canLocatePlaces => Future.wait([isConnected, hasPlayServices]).then((results) => results.every((result) => result)); + Future get canLocatePlaces async => mobileServices.isServiceAvailable && await isConnected; @override - Future get canUseGoogleMaps async => device.canRenderGoogleMaps && await hasPlayServices; + List get mapStyles => [ + ...mobileServices.mapStyles, + ...EntryMapStyle.values.where((v) => !v.needMobileService), + ]; } diff --git a/lib/model/db/db_metadata_sqflite.dart b/lib/model/db/db_metadata_sqflite.dart index 854be034d..1acb8b5b1 100644 --- a/lib/model/db/db_metadata_sqflite.dart +++ b/lib/model/db/db_metadata_sqflite.dart @@ -220,8 +220,8 @@ class SqfliteMetadataDb implements MetadataDb { Future> searchLiveEntries(String query, {int? limit}) async { final rows = await _db.query( entryTable, - where: 'title LIKE ? AND trashed = ?', - whereArgs: ['%$query%', 0], + where: '(title LIKE ? OR path LIKE ?) AND trashed = ?', + whereArgs: ['%$query%', '%$query%', 0], orderBy: 'sourceDateTakenMillis DESC', limit: limit, ); diff --git a/lib/model/device.dart b/lib/model/device.dart index e26583f61..ab288f972 100644 --- a/lib/model/device.dart +++ b/lib/model/device.dart @@ -5,7 +5,7 @@ final Device device = Device._private(); class Device { late final String _userAgent; - late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderFlagEmojis, _canRenderGoogleMaps; + late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderFlagEmojis; late final bool _showPinShortcutFeedback, _supportEdgeToEdgeUIMode; String get userAgent => _userAgent; @@ -18,8 +18,6 @@ class Device { bool get canRenderFlagEmojis => _canRenderFlagEmojis; - bool get canRenderGoogleMaps => _canRenderGoogleMaps; - bool get showPinShortcutFeedback => _showPinShortcutFeedback; bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode; @@ -35,7 +33,6 @@ class Device { _canPinShortcut = capabilities['canPinShortcut'] ?? false; _canPrint = capabilities['canPrint'] ?? false; _canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false; - _canRenderGoogleMaps = capabilities['canRenderGoogleMaps'] ?? false; _showPinShortcutFeedback = capabilities['showPinShortcutFeedback'] ?? false; _supportEdgeToEdgeUIMode = capabilities['supportEdgeToEdgeUIMode'] ?? false; } diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 262d8d24c..7e9d89805 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -584,7 +584,7 @@ class AvesEntry { : call()); if (addresses.isNotEmpty) { final address = addresses.first; - final cc = address.countryCode; + final cc = address.countryCode?.toUpperCase(); final cn = address.countryName; final aa = address.adminArea; addressDetails = AddressDetails( diff --git a/lib/model/entry_images.dart b/lib/model/entry_images.dart index c10467f77..2edd131ce 100644 --- a/lib/model/entry_images.dart +++ b/lib/model/entry_images.dart @@ -55,7 +55,7 @@ extension ExtraAvesEntryImages on AvesEntry { expectedContentLength: sizeBytes, ); - bool _isReady(Object providerKey) => imageCache!.statusForKey(providerKey).keepAlive; + bool _isReady(Object providerKey) => imageCache.statusForKey(providerKey).keepAlive; List get cachedThumbnails => EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).where(_isReady).map(ThumbnailProvider.new).toList(); diff --git a/lib/model/entry_metadata_edition.dart b/lib/model/entry_metadata_edition.dart index 33ae00229..a1f8bf829 100644 --- a/lib/model/entry_metadata_edition.dart +++ b/lib/model/entry_metadata_edition.dart @@ -22,7 +22,9 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { final appliedModifier = await _applyDateModifierToEntry(userModifier); if (appliedModifier == null) { - await reportService.recordError('failed to get date for modifier=$userModifier, entry=$this', null); + if (!isMissingAtPath) { + await reportService.recordError('failed to get date for modifier=$userModifier, entry=$this', null); + } return {}; } @@ -374,7 +376,12 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry { switch (source) { case DateFieldSource.fileModifiedDate: try { - date = path != null ? await File(path!).lastModified() : null; + if (path != null) { + final file = File(path!); + if (await file.exists()) { + date = await file.lastModified(); + } + } } on FileSystemException catch (_) {} break; default: diff --git a/lib/model/filters/coordinate.dart b/lib/model/filters/coordinate.dart index 90664b8c0..5b42c7e41 100644 --- a/lib/model/filters/coordinate.dart +++ b/lib/model/filters/coordinate.dart @@ -4,8 +4,8 @@ import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/geo_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index 9bdca8654..17a94a947 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -84,7 +84,7 @@ class LocationFilter extends CoveredCollectionFilter { static String? countryCodeToFlag(String? code) { if (code == null || code.length != 2) return null; - return String.fromCharCodes(code.codeUnits.map((letter) => letter += _countryCodeToFlagDiff)); + return String.fromCharCodes(code.toUpperCase().codeUnits.map((letter) => letter += _countryCodeToFlagDiff)); } } diff --git a/lib/model/geotiff.dart b/lib/model/geotiff.dart index f86c2a909..dc3ab8deb 100644 --- a/lib/model/geotiff.dart +++ b/lib/model/geotiff.dart @@ -5,9 +5,8 @@ import 'dart:ui' as ui; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; import 'package:aves/ref/geotiff.dart'; -import 'package:aves/utils/geo_utils.dart'; import 'package:aves/utils/math_utils.dart'; -import 'package:aves/widgets/common/map/tile.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; @@ -40,7 +39,7 @@ class GeoTiffInfo extends Equatable { } } -class MappedGeoTiff { +class MappedGeoTiff with MapOverlay { final AvesEntry entry; late LatLng? Function(Point pixel) pointToLatLng; late Point? Function(Point smPoint) epsg3857ToPoint; @@ -129,6 +128,7 @@ class MappedGeoTiff { }; } + @override Future getTile(int tx, int ty, int? zoomLevel) async { zoomLevel ??= 0; @@ -217,15 +217,24 @@ class MappedGeoTiff { ); } + @override + String get id => entry.uri; + + @override + ImageProvider get imageProvider => entry.uriImage; + int get width => entry.width; int get height => entry.height; + @override bool get canOverlay => center != null; LatLng? get center => pointToLatLng(Point((width / 2).round(), (height / 2).round())); + @override LatLng? get topLeft => pointToLatLng(const Point(0, 0)); + @override LatLng? get bottomRight => pointToLatLng(Point(width, height)); } diff --git a/lib/model/selection.dart b/lib/model/selection.dart index 2d19227ed..f26d0c4d9 100644 --- a/lib/model/selection.dart +++ b/lib/model/selection.dart @@ -11,13 +11,15 @@ class Selection extends ChangeNotifier { void browse() { if (!_isSelecting) return; - clearSelection(); _isSelecting = false; notifyListeners(); } void select() { if (_isSelecting) return; + // clear selection on `select`, not on `browse`, so that + // the selection count is stable when transitioning to browse + clearSelection(); _isSelecting = true; notifyListeners(); } @@ -42,7 +44,7 @@ class Selection extends ChangeNotifier { } void toggleSelection(T item) { - if (_selectedItems.isEmpty) select(); + if (!_isSelecting) select(); if (!_selectedItems.remove(item)) _selectedItems.add(item); notifyListeners(); } diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index 21d5baddd..ac2fdfc35 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -8,6 +8,7 @@ import 'package:aves/model/source/enums.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; class SettingsDefaults { @@ -26,6 +27,7 @@ class SettingsDefaults { static const mustBackTwiceToExit = true; static const keepScreenOn = KeepScreenOn.viewerOnly; static const homePage = HomePageSetting.collection; + static const showBottomNavigationBar = true; static const confirmDeleteForever = true; static const confirmMoveToBin = true; static const confirmMoveUndatedItems = true; @@ -52,6 +54,7 @@ class SettingsDefaults { EntrySetAction.delete, ]; static const showThumbnailFavourite = true; + static const showThumbnailTag = false; static const showThumbnailLocation = true; static const showThumbnailMotionPhoto = true; static const showThumbnailRating = true; diff --git a/lib/model/settings/enums/accessibility_timeout.dart b/lib/model/settings/enums/accessibility_timeout.dart index b5ef55d78..d9b9f1ebd 100644 --- a/lib/model/settings/enums/accessibility_timeout.dart +++ b/lib/model/settings/enums/accessibility_timeout.dart @@ -10,6 +10,8 @@ extension ExtraAccessibilityTimeout on AccessibilityTimeout { return context.l10n.settingsSystemDefault; case AccessibilityTimeout.appDefault: return context.l10n.settingsDefault; + case AccessibilityTimeout.s3: + return context.l10n.timeSeconds(3); case AccessibilityTimeout.s10: return context.l10n.timeSeconds(10); case AccessibilityTimeout.s30: diff --git a/lib/model/settings/enums/coordinate_format.dart b/lib/model/settings/enums/coordinate_format.dart index 1ff17855f..4b3b2ffd9 100644 --- a/lib/model/settings/enums/coordinate_format.dart +++ b/lib/model/settings/enums/coordinate_format.dart @@ -1,5 +1,4 @@ import 'package:aves/l10n/l10n.dart'; -import 'package:aves/utils/geo_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; @@ -33,14 +32,32 @@ extension ExtraCoordinateFormat on CoordinateFormat { final locale = l10n.localeName; final lat = latLng.latitude; final lng = latLng.longitude; - final latSexa = GeoUtils.decimal2sexagesimal(lat, minuteSecondPadding, secondDecimals, locale); - final lngSexa = GeoUtils.decimal2sexagesimal(lng, minuteSecondPadding, secondDecimals, locale); + final latSexa = _decimal2sexagesimal(lat, minuteSecondPadding, secondDecimals, locale); + final lngSexa = _decimal2sexagesimal(lng, minuteSecondPadding, secondDecimals, locale); return [ l10n.coordinateDms(latSexa, lat < 0 ? l10n.coordinateDmsSouth : l10n.coordinateDmsNorth), l10n.coordinateDms(lngSexa, lng < 0 ? l10n.coordinateDmsWest : l10n.coordinateDmsEast), ]; } + static String _decimal2sexagesimal( + double degDecimal, + bool minuteSecondPadding, + int secondDecimals, + String locale, + ) { + final degAbs = degDecimal.abs(); + final deg = degAbs.toInt(); + final minDecimal = (degAbs - deg) * 60; + final min = minDecimal.toInt(); + final sec = (minDecimal - min) * 60; + + var minText = NumberFormat('0' * (minuteSecondPadding ? 2 : 1), locale).format(min); + var secText = NumberFormat('${'0' * (minuteSecondPadding ? 2 : 1)}${secondDecimals > 0 ? '.${'0' * secondDecimals}' : ''}', locale).format(sec); + + return '$deg° $minText′ $secText″'; + } + static List _toDecimal(AppLocalizations l10n, LatLng latLng) { final locale = l10n.localeName; final formatter = NumberFormat('0.000000°', locale); diff --git a/lib/model/settings/enums/enums.dart b/lib/model/settings/enums/enums.dart index 461541d2c..6ec4c8714 100644 --- a/lib/model/settings/enums/enums.dart +++ b/lib/model/settings/enums/enums.dart @@ -1,6 +1,6 @@ enum AccessibilityAnimations { system, disabled, enabled } -enum AccessibilityTimeout { system, appDefault, s10, s30, s60, s120 } +enum AccessibilityTimeout { system, appDefault, s3, s10, s30, s60, s120 } enum AvesThemeColorMode { monochrome, polychrome } @@ -12,9 +12,6 @@ enum CoordinateFormat { dms, decimal } enum EntryBackground { black, white, checkered } -// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ -enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor } - enum HomePageSetting { collection, albums } enum KeepScreenOn { never, viewerOnly, always } diff --git a/lib/model/settings/enums/map_style.dart b/lib/model/settings/enums/map_style.dart index fa5f8eedb..3bed08e31 100644 --- a/lib/model/settings/enums/map_style.dart +++ b/lib/model/settings/enums/map_style.dart @@ -1,8 +1,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/widgets.dart'; -import 'enums.dart'; - extension ExtraEntryMapStyle on EntryMapStyle { String getName(BuildContext context) { switch (this) { @@ -12,6 +11,10 @@ extension ExtraEntryMapStyle on EntryMapStyle { return context.l10n.mapStyleGoogleHybrid; case EntryMapStyle.googleTerrain: return context.l10n.mapStyleGoogleTerrain; + case EntryMapStyle.hmsNormal: + return context.l10n.mapStyleHuaweiNormal; + case EntryMapStyle.hmsTerrain: + return context.l10n.mapStyleHuaweiTerrain; case EntryMapStyle.osmHot: return context.l10n.mapStyleOsmHot; case EntryMapStyle.stamenToner: @@ -21,14 +24,27 @@ extension ExtraEntryMapStyle on EntryMapStyle { } } - bool get isGoogleMaps { + bool get isHeavy { switch (this) { case EntryMapStyle.googleNormal: case EntryMapStyle.googleHybrid: case EntryMapStyle.googleTerrain: + case EntryMapStyle.hmsNormal: + case EntryMapStyle.hmsTerrain: return true; default: return false; } } + + bool get needMobileService { + switch (this) { + case EntryMapStyle.osmHot: + case EntryMapStyle.stamenToner: + case EntryMapStyle.stamenWatercolor: + return false; + default: + return true; + } + } } diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index f3f4df159..6c0617863 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -11,6 +11,7 @@ import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/services/accessibility_service.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -54,6 +55,7 @@ class Settings extends ChangeNotifier { static const mustBackTwiceToExitKey = 'must_back_twice_to_exit'; static const keepScreenOnKey = 'keep_screen_on'; static const homePageKey = 'home_page'; + static const showBottomNavigationBarKey = 'show_bottom_navigation_bar'; static const confirmDeleteForeverKey = 'confirm_delete_forever'; static const confirmMoveToBinKey = 'confirm_move_to_bin'; static const confirmMoveUndatedItemsKey = 'confirm_move_undated_items'; @@ -68,6 +70,7 @@ class Settings extends ChangeNotifier { static const collectionBrowsingQuickActionsKey = 'collection_browsing_quick_actions'; static const collectionSelectionQuickActionsKey = 'collection_selection_quick_actions'; static const showThumbnailFavouriteKey = 'show_thumbnail_favourite'; + static const showThumbnailTagKey = 'show_thumbnail_tag'; static const showThumbnailLocationKey = 'show_thumbnail_location'; static const showThumbnailMotionPhotoKey = 'show_thumbnail_motion_photo'; static const showThumbnailRatingKey = 'show_thumbnail_rating'; @@ -161,11 +164,11 @@ class Settings extends ChangeNotifier { enableOverlayBlurEffect = performanceClass >= 29; // availability - final canUseGoogleMaps = await availability.canUseGoogleMaps; - if (canUseGoogleMaps) { - infoMapStyle = EntryMapStyle.googleNormal; + final defaultMapStyle = mobileServices.defaultMapStyle; + if (mobileServices.mapStyles.contains(defaultMapStyle)) { + infoMapStyle = defaultMapStyle; } else { - final styles = EntryMapStyle.values.whereNot((v) => v.isGoogleMaps).toList(); + final styles = EntryMapStyle.values.whereNot((v) => v.needMobileService).toList(); infoMapStyle = styles[Random().nextInt(styles.length)]; } @@ -235,7 +238,7 @@ class Settings extends ChangeNotifier { if (_locale != null) { preferredLocales.add(_locale); } else { - preferredLocales.addAll(WidgetsBinding.instance!.window.locales); + preferredLocales.addAll(WidgetsBinding.instance.window.locales); if (preferredLocales.isEmpty) { // the `window` locales may be empty in a window-less service context preferredLocales.addAll(_systemLocalesFallback); @@ -292,6 +295,10 @@ class Settings extends ChangeNotifier { set homePage(HomePageSetting newValue) => setAndNotify(homePageKey, newValue.toString()); + bool get showBottomNavigationBar => getBoolOrDefault(showBottomNavigationBarKey, SettingsDefaults.showBottomNavigationBar); + + set showBottomNavigationBar(bool newValue) => setAndNotify(showBottomNavigationBarKey, newValue); + bool get confirmDeleteForever => getBoolOrDefault(confirmDeleteForeverKey, SettingsDefaults.confirmDeleteForever); set confirmDeleteForever(bool newValue) => setAndNotify(confirmDeleteForeverKey, newValue); @@ -347,6 +354,10 @@ class Settings extends ChangeNotifier { set showThumbnailFavourite(bool newValue) => setAndNotify(showThumbnailFavouriteKey, newValue); + bool get showThumbnailTag => getBoolOrDefault(showThumbnailTagKey, SettingsDefaults.showThumbnailTag); + + set showThumbnailTag(bool newValue) => setAndNotify(showThumbnailTagKey, newValue); + bool get showThumbnailLocation => getBoolOrDefault(showThumbnailLocationKey, SettingsDefaults.showThumbnailLocation); set showThumbnailLocation(bool newValue) => setAndNotify(showThumbnailLocationKey, newValue); @@ -504,7 +515,11 @@ class Settings extends ChangeNotifier { // info - EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, SettingsDefaults.infoMapStyle, EntryMapStyle.values); + EntryMapStyle get infoMapStyle { + final preferred = getEnumOrDefault(infoMapStyleKey, SettingsDefaults.infoMapStyle, EntryMapStyle.values); + final available = availability.mapStyles; + return available.contains(preferred) ? preferred : available.first; + } set infoMapStyle(EntryMapStyle newValue) => setAndNotify(infoMapStyleKey, newValue.toString()); @@ -680,12 +695,14 @@ class Settings extends ChangeNotifier { break; case isInstalledAppAccessAllowedKey: case isErrorReportingAllowedKey: + case showBottomNavigationBarKey: case mustBackTwiceToExitKey: case confirmDeleteForeverKey: case confirmMoveToBinKey: case confirmMoveUndatedItemsKey: case setMetadataDateBeforeFileOpKey: case showThumbnailFavouriteKey: + case showThumbnailTagKey: case showThumbnailLocationKey: case showThumbnailMotionPhotoKey: case showThumbnailRatingKey: diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index 3808fe9d9..1fd58d204 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.dart @@ -26,9 +26,11 @@ mixin AlbumMixin on SourceBase { return compareAsciiUpperCase(va, vb); } - void _onAlbumChanged() { + void _onAlbumChanged({bool notify = true}) { invalidateAlbumDisplayNames(); - eventBus.fire(AlbumsChangedEvent()); + if (notify) { + eventBus.fire(AlbumsChangedEvent()); + } } Map getAlbumEntries() { @@ -55,14 +57,14 @@ mixin AlbumMixin on SourceBase { void updateDirectories() { final visibleDirectories = visibleEntries.map((entry) => entry.directory).toSet(); - addDirectories(visibleDirectories); + addDirectories(albums: visibleDirectories); cleanEmptyAlbums(); } - void addDirectories(Set albums) { + void addDirectories({required Set albums, bool notify = true}) { if (!_directories.containsAll(albums)) { _directories.addAll(albums); - _onAlbumChanged(); + _onAlbumChanged(notify: notify); } } @@ -92,7 +94,11 @@ mixin AlbumMixin on SourceBase { final Map _filterEntryCountMap = {}; final Map _filterRecentEntryMap = {}; - void invalidateAlbumFilterSummary({Set? entries, Set? directories}) { + void invalidateAlbumFilterSummary({ + Set? entries, + Set? directories, + bool notify = true, + }) { if (_filterEntryCountMap.isEmpty && _filterRecentEntryMap.isEmpty) return; if (entries == null && directories == null) { @@ -108,7 +114,9 @@ mixin AlbumMixin on SourceBase { _filterRecentEntryMap.remove(directory); }); } - eventBus.fire(AlbumSummaryInvalidatedEvent(directories)); + if (notify) { + eventBus.fire(AlbumSummaryInvalidatedEvent(directories)); + } } int albumEntryCount(AlbumFilter filter) { @@ -123,7 +131,7 @@ mixin AlbumMixin on SourceBase { void createAlbum(String directory) { _newAlbums.add(directory); - addDirectories({directory}); + addDirectories(albums: {directory}); } void renameNewAlbum(String source, String destination) { diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index cd8d69453..2ce5a6f79 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -43,6 +43,8 @@ mixin SourceBase { ValueNotifier progressNotifier = ValueNotifier(const ProgressEvent(done: 0, total: 0)); void setProgress({required int done, required int total}) => progressNotifier.value = ProgressEvent(done: done, total: total); + + void invalidateEntries(); } abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin, TrashMixin { @@ -112,17 +114,22 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM return entries.where(TrashFilter.instance.test); } - void _invalidate([Set? entries]) { + void _invalidate({Set? entries, bool notify = true}) { + invalidateEntries(); + invalidateAlbumFilterSummary(entries: entries, notify: notify); + invalidateCountryFilterSummary(entries: entries, notify: notify); + invalidateTagFilterSummary(entries: entries, notify: notify); + } + + @override + void invalidateEntries() { _visibleEntries = null; _trashedEntries = null; _sortedEntriesByDate = null; - invalidateAlbumFilterSummary(entries: entries); - invalidateCountryFilterSummary(entries: entries); - invalidateTagFilterSummary(entries: entries); } void updateDerivedFilters([Set? entries]) { - _invalidate(entries); + _invalidate(entries: entries); // it is possible for entries hidden by a filter type, to have an impact on other types // e.g. given a sole entry for country C and tag T, hiding T should make C disappear too updateDirectories(); @@ -130,7 +137,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM updateTags(); } - void addEntries(Set entries) { + void addEntries(Set entries, {bool notify = true}) { if (entries.isEmpty) return; final newIdMapEntries = Map.fromEntries(entries.map((entry) => MapEntry(entry.id, entry))); @@ -145,10 +152,12 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM _entryById.addAll(newIdMapEntries); _rawEntries.addAll(entries); - _invalidate(entries); + _invalidate(entries: entries, notify: notify); - addDirectories(_applyHiddenFilters(entries).map((entry) => entry.directory).toSet()); - eventBus.fire(EntryAddedEvent(entries)); + addDirectories(albums: _applyHiddenFilters(entries).map((entry) => entry.directory).toSet(), notify: notify); + if (notify) { + eventBus.fire(EntryAddedEvent(entries)); + } } Future removeEntries(Set uris, {required bool includeTrash}) async { @@ -320,7 +329,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM case MoveType.move: case MoveType.export: cleanEmptyAlbums(fromAlbums); - addDirectories(destinationAlbums); + addDirectories(albums: destinationAlbums); break; case MoveType.toBin: case MoveType.fromBin: @@ -328,7 +337,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM break; } invalidateAlbumFilterSummary(directories: fromAlbums); - _invalidate(movedEntries); + _invalidate(entries: movedEntries); eventBus.fire(EntryMovedEvent(moveType, movedEntries)); } diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index 8eb3f8fc7..144cb683b 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -24,6 +24,7 @@ mixin LocationMixin on SourceBase { final saved = await (ids != null ? metadataDb.loadAddressesById(ids) : metadataDb.loadAddresses()); final idMap = entryById; saved.forEach((metadata) => idMap[metadata.id]?.addressDetails = metadata); + invalidateEntries(); onAddressMetadataChanged(); } @@ -182,7 +183,11 @@ mixin LocationMixin on SourceBase { final Map _filterEntryCountMap = {}; final Map _filterRecentEntryMap = {}; - void invalidateCountryFilterSummary({Set? entries, Set? countryCodes}) { + void invalidateCountryFilterSummary({ + Set? entries, + Set? countryCodes, + bool notify = true, + }) { if (_filterEntryCountMap.isEmpty && _filterRecentEntryMap.isEmpty) return; if (entries == null && countryCodes == null) { @@ -198,7 +203,9 @@ mixin LocationMixin on SourceBase { _filterRecentEntryMap.remove(countryCode); }); } - eventBus.fire(CountrySummaryInvalidatedEvent(countryCodes)); + if (notify) { + eventBus.fire(CountrySummaryInvalidatedEvent(countryCodes)); + } } int countryEntryCount(LocationFilter filter) { diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 48af4d718..8bd93063f 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -96,7 +96,9 @@ class MediaStoreSource extends CollectionSource { // show known entries debugPrint('$runtimeType refresh ${stopwatch.elapsed} add known entries'); - addEntries(knownEntries); + // add entries without notifying, so that the collection is not refreshed + // with items that may be hidden right away because of their metadata + addEntries(knownEntries, notify: false); debugPrint('$runtimeType refresh ${stopwatch.elapsed} load metadata'); if (directory != null) { diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index b2fa61907..c092600cf 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -18,6 +18,7 @@ mixin TagMixin on SourceBase { final saved = await (ids != null ? metadataDb.loadCatalogMetadataById(ids) : metadataDb.loadCatalogMetadata()); final idMap = entryById; saved.forEach((metadata) => idMap[metadata.id]?.catalogMetadata = metadata); + invalidateEntries(); onCatalogMetadataChanged(); } @@ -77,7 +78,11 @@ mixin TagMixin on SourceBase { final Map _filterEntryCountMap = {}; final Map _filterRecentEntryMap = {}; - void invalidateTagFilterSummary({Set? entries, Set? tags}) { + void invalidateTagFilterSummary({ + Set? entries, + Set? tags, + bool notify = true, + }) { if (_filterEntryCountMap.isEmpty && _filterRecentEntryMap.isEmpty) return; if (entries == null && tags == null) { @@ -93,7 +98,9 @@ mixin TagMixin on SourceBase { _filterRecentEntryMap.remove(tag); }); } - eventBus.fire(TagSummaryInvalidatedEvent(tags)); + if (notify) { + eventBus.fire(TagSummaryInvalidatedEvent(tags)); + } } int tagEntryCount(TagFilter filter) { diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index 1b7564ba3..e5e7f1a00 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -24,6 +24,9 @@ import 'package:flutter/foundation.dart'; class VideoMetadataFormatter { static final _dateY4M2D2H2m2s2Pattern = RegExp(r'(\d{4})[-/](\d{2})[-/](\d{2}) (\d{2}):(\d{2}):(\d{2})'); static final _dateY4M2D2H2m2s2APmPattern = RegExp(r'(\d{4})[-/](\d{2})[-/](\d{2})T(\d+):(\d+):(\d+) ([ap]m)Z'); + static final _ambiguousDatePatterns = { + RegExp(r'^\d{2}[-/]\d{2}[-/]\d{4}$'), + }; static final _durationPattern = RegExp(r'(\d+):(\d+):(\d+)(.\d+)'); static final _locationPattern = RegExp(r'([+-][.0-9]+)'); static final Map _codecNames = { @@ -93,7 +96,7 @@ class VideoMetadataFormatter { int? dateMillis; if (isDefined(dateString)) { dateMillis = parseVideoDate(dateString); - if (dateMillis == null) { + if (dateMillis == null && !isAmbiguousDate(dateString)) { await reportService.recordError('getCatalogMetadata failed to parse date=$dateString for mimeType=${entry.mimeType} entry=$entry', null); } } @@ -107,6 +110,10 @@ class VideoMetadataFormatter { return entry.catalogMetadata; } + static bool isAmbiguousDate(String dateString) { + return _ambiguousDatePatterns.any((pattern) => pattern.hasMatch(dateString)); + } + static int? parseVideoDate(String dateString) { final date = DateTime.tryParse(dateString); if (date != null) { diff --git a/lib/ref/mime_types.dart b/lib/ref/mime_types.dart index 185165bec..cec770cae 100644 --- a/lib/ref/mime_types.dart +++ b/lib/ref/mime_types.dart @@ -1,6 +1,7 @@ class MimeTypes { static const anyImage = 'image/*'; + static const avif = 'image/avif'; static const bmp = 'image/bmp'; static const bmpX = 'image/x-ms-bmp'; static const gif = 'image/gif'; @@ -14,6 +15,7 @@ class MimeTypes { static const webp = 'image/webp'; static const art = 'image/x-jg'; + static const cdr = 'image/x-coreldraw'; static const djvu = 'image/vnd.djvu'; static const jxl = 'image/jxl'; static const psdVnd = 'image/vnd.adobe.photoshop'; @@ -65,23 +67,33 @@ class MimeTypes { // groups // formats that support transparency - static const Set alphaImages = {bmp, bmpX, gif, ico, png, svg, tiff, webp}; + static const Set alphaImages = {avif, bmp, bmpX, gif, heic, heif, ico, png, svg, tiff, webp}; static const Set rawImages = {arw, cr2, crw, dcr, dng, erf, k25, kdc, mrw, nef, nrw, orf, pef, raf, raw, rw2, sr2, srf, srw, x3f}; // TODO TLAD [codec] make it dynamic if it depends on OS/lib versions - static const Set undecodableImages = {art, crw, djvu, jxl, psdVnd, psdX, octetStream, zip}; + static const Set undecodableImages = {art, cdr, crw, djvu, jxl, psdVnd, psdX, octetStream, zip}; - static const Set _knownOpaqueImages = {heic, heif, jpeg}; + static const Set _knownOpaqueImages = {jpeg}; static const Set _knownVideos = {avi, aviVnd, flv, flvX, mkv, mov, mp2t, mp2ts, mp4, mpeg, ogv, webm}; - static final Set knownMediaTypes = {..._knownOpaqueImages, ...alphaImages, ...rawImages, ...undecodableImages, ..._knownVideos}; + static final Set knownMediaTypes = { + anyImage, + ..._knownOpaqueImages, + ...alphaImages, + ...rawImages, + ...undecodableImages, + anyVideo, + ..._knownVideos, + }; static bool isImage(String mimeType) => mimeType.startsWith('image'); static bool isVideo(String mimeType) => mimeType.startsWith('video'); + static bool isVisual(String mimeType) => isImage(mimeType) || isVideo(mimeType); + static bool refersToSameType(String a, b) { switch (a) { case avi: diff --git a/lib/services/analysis_service.dart b/lib/services/analysis_service.dart index 95b8eb752..7c75aa6e4 100644 --- a/lib/services/analysis_service.dart +++ b/lib/services/analysis_service.dart @@ -43,6 +43,7 @@ Future _init() async { WidgetsFlutterBinding.ensureInitialized(); initPlatformServices(); await metadataDb.init(); + await mobileServices.init(); await settings.init(monitorPlatformSettings: false); FijkLog.setLevel(FijkLogLevel.Warn); await reportService.init(); diff --git a/lib/services/common/image_op_events.dart b/lib/services/common/image_op_events.dart index 13863ad3f..60f055a1c 100644 --- a/lib/services/common/image_op_events.dart +++ b/lib/services/common/image_op_events.dart @@ -28,29 +28,29 @@ class ImageOpEvent extends Equatable { @immutable class MoveOpEvent extends ImageOpEvent { final Map newFields; + final bool deleted; @override - List get props => [success, skipped, uri, newFields]; + List get props => [success, skipped, uri, newFields, deleted]; const MoveOpEvent({ - required bool success, - required bool skipped, - required String uri, + required super.success, + required super.skipped, + required super.uri, required this.newFields, - }) : super( - success: success, - skipped: skipped, - uri: uri, - ); + required this.deleted, + }); factory MoveOpEvent.fromMap(Map map) { final newFields = map['newFields'] ?? {}; final skipped = (map['skipped'] ?? false) || (newFields['skipped'] ?? false); + final deleted = (map['deleted'] ?? false) || (newFields['deleted'] ?? false); return MoveOpEvent( success: (map['success'] ?? false) || skipped, skipped: skipped, uri: map['uri'], newFields: newFields, + deleted: deleted, ); } } @@ -63,16 +63,13 @@ class ExportOpEvent extends MoveOpEvent { List get props => [success, skipped, uri, pageId, newFields]; const ExportOpEvent({ - required bool success, - required bool skipped, - required String uri, + required super.success, + required super.skipped, + required super.uri, this.pageId, - required Map newFields, + required super.newFields, }) : super( - success: success, - skipped: skipped, - uri: uri, - newFields: newFields, + deleted: false, ); factory ExportOpEvent.fromMap(Map map) { diff --git a/lib/services/common/services.dart b/lib/services/common/services.dart index 12f817a61..9a2a1e062 100644 --- a/lib/services/common/services.dart +++ b/lib/services/common/services.dart @@ -14,6 +14,8 @@ import 'package:aves/services/storage_service.dart'; import 'package:aves/services/window_service.dart'; import 'package:aves_report/aves_report.dart'; import 'package:aves_report_platform/aves_report_platform.dart'; +import 'package:aves_services/aves_services.dart'; +import 'package:aves_services_platform/aves_services_platform.dart'; import 'package:get_it/get_it.dart'; import 'package:path/path.dart' as p; @@ -33,6 +35,7 @@ final MediaFileService mediaFileService = getIt(); final MediaStoreService mediaStoreService = getIt(); final MetadataEditService metadataEditService = getIt(); final MetadataFetchService metadataFetchService = getIt(); +final MobileServices mobileServices = getIt(); final ReportService reportService = getIt(); final StorageService storageService = getIt(); final WindowService windowService = getIt(); @@ -49,6 +52,7 @@ void initPlatformServices() { getIt.registerLazySingleton(PlatformMediaStoreService.new); getIt.registerLazySingleton(PlatformMetadataEditService.new); getIt.registerLazySingleton(PlatformMetadataFetchService.new); + getIt.registerLazySingleton(PlatformMobileServices.new); getIt.registerLazySingleton(PlatformReportService.new); getIt.registerLazySingleton(PlatformStorageService.new); getIt.registerLazySingleton(PlatformWindowService.new); diff --git a/lib/services/media/media_file_service.dart b/lib/services/media/media_file_service.dart index 00020a2de..ae4542659 100644 --- a/lib/services/media/media_file_service.dart +++ b/lib/services/media/media_file_service.dart @@ -211,7 +211,7 @@ class PlatformMediaFileService implements MediaFileService { // `await` here, so that `completeError` will be caught below return await completer.future; } on PlatformException catch (e, stack) { - if (!MimeTypes.knownMediaTypes.contains(mimeType)) { + if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { await reportService.recordError(e, stack); } } @@ -248,7 +248,9 @@ class PlatformMediaFileService implements MediaFileService { }); if (result != null) return result as Uint8List; } on PlatformException catch (e, stack) { - await reportService.recordError(e, stack); + if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { + await reportService.recordError(e, stack); + } } return Uint8List(0); }, @@ -285,7 +287,7 @@ class PlatformMediaFileService implements MediaFileService { }); if (result != null) return result as Uint8List; } on PlatformException catch (e, stack) { - if (!MimeTypes.knownMediaTypes.contains(mimeType)) { + if (!MimeTypes.knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType)) { await reportService.recordError(e, stack); } } diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index f1af3e4d4..6c7b8e45a 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -200,7 +200,14 @@ class PlatformStorageService implements StorageService { // `await` here, so that `completeError` will be caught below return await completer.future; } on PlatformException catch (e, stack) { - await reportService.recordError(e, stack); + final message = e.message; + // mute issue in the specific case when an item: + // 1) is a Media Store `file` content, + // 2) has no `images` or `video` entry, + // 3) is in a restricted directory + if (message == null || !message.contains('/external/file/')) { + await reportService.recordError(e, stack); + } } return false; } diff --git a/lib/services/viewer_service.dart b/lib/services/viewer_service.dart index 7956e3ae3..335bd7b71 100644 --- a/lib/services/viewer_service.dart +++ b/lib/services/viewer_service.dart @@ -15,10 +15,10 @@ class ViewerService { return {}; } - static Future pick(String uri) async { + static Future pick(List uris) async { try { await platform.invokeMethod('pick', { - 'uri': uri, + 'uris': uris, }); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index af6d581ec..9782413e0 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -11,9 +11,9 @@ class AvesColorsProvider extends StatelessWidget { final Widget child; const AvesColorsProvider({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index c13392c3c..cf0562ea5 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -74,9 +74,9 @@ class DurationsProvider extends StatelessWidget { final Widget child; const DurationsProvider({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index c0e66ff4f..3bc9c645f 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -10,6 +10,7 @@ class AIcons { static const IconData accessibility = Icons.accessibility_new_outlined; static const IconData android = Icons.android; static const IconData app = Icons.apps_outlined; + static const IconData apply = Icons.done_outlined; static const IconData bin = Icons.delete_outlined; static const IconData broken = Icons.broken_image_outlined; static const IconData checked = Icons.done_outlined; diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index b84506d91..443e47935 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -29,6 +29,8 @@ class Constants { // Pop Directional Isolate static const pdi = '\u2069'; + static const zwsp = '\u200B'; + static const overlayUnknown = '—'; // em dash static final pointNemo = LatLng(-48.876667, -123.393333); @@ -116,17 +118,6 @@ class Constants { license: 'MIT', sourceUrl: 'https://github.com/ajinasokan/flutter_displaymode', ), - Dependency( - name: 'Google API Availability', - license: 'MIT', - sourceUrl: 'https://github.com/Baseflow/flutter-google-api-availability', - ), - Dependency( - name: 'Google Maps for Flutter', - license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter', - ), Dependency( name: 'Package Info Plus', license: 'BSD 3-Clause', @@ -172,7 +163,39 @@ class Constants { ), ]; + static const List _googleMobileServices = [ + Dependency( + name: 'Google API Availability', + license: 'MIT', + sourceUrl: 'https://github.com/Baseflow/flutter-google-api-availability', + ), + Dependency( + name: 'Google Maps for Flutter', + license: 'BSD 3-Clause', + licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter/LICENSE', + sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter', + ), + ]; + + static const List _huaweiMobileServices = [ + Dependency( + name: 'Huawei Mobile Services (Availability, Map)', + license: 'Apache 2.0', + licenseUrl: 'https://github.com/HMS-Core/hms-flutter-plugin/blob/master/LICENCE', + sourceUrl: 'https://github.com/HMS-Core/hms-flutter-plugin', + ), + ]; + + static const List _flutterPluginsHuaweiOnly = [ + ..._huaweiMobileServices, + ]; + + static const List _flutterPluginsIzzyOnly = [ + ..._googleMobileServices, + ]; + static const List _flutterPluginsPlayOnly = [ + ..._googleMobileServices, Dependency( name: 'FlutterFire (Core, Crashlytics)', license: 'BSD 3-Clause', @@ -182,6 +205,8 @@ class Constants { static List flutterPlugins(AppFlavor flavor) => [ ..._flutterPluginsCommon, + if (flavor == AppFlavor.huawei) ..._flutterPluginsHuaweiOnly, + if (flavor == AppFlavor.izzy) ..._flutterPluginsIzzyOnly, if (flavor == AppFlavor.play) ..._flutterPluginsPlayOnly, ]; @@ -312,6 +337,11 @@ class Constants { license: 'Apache 2.0', sourceUrl: 'https://github.com/jifalops/dart-latlong', ), + Dependency( + name: 'Path', + license: 'BSD 3-Clause', + sourceUrl: 'https://github.com/dart-lang/path', + ), Dependency( name: 'PDF for Dart and Flutter', license: 'Apache 2.0', diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart index 3e69444b5..3766f7f82 100644 --- a/lib/widgets/about/about_page.dart +++ b/lib/widgets/about/about_page.dart @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; class AboutPage extends StatelessWidget { static const routeName = '/about'; - const AboutPage({Key? key}) : super(key: key); + const AboutPage({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart index b7d1f253e..e8ac7ef89 100644 --- a/lib/widgets/about/app_ref.dart +++ b/lib/widgets/about/app_ref.dart @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; class AppReference extends StatefulWidget { - const AppReference({Key? key}) : super(key: key); + const AppReference({super.key}); @override State createState() => _AppReferenceState(); @@ -79,7 +79,7 @@ class _AppReferenceState extends State { size: 24, ), text: l10n.aboutLinkSources, - url: Constants.avesGithub, + urlString: Constants.avesGithub, ), LinkChip( leading: const Icon( @@ -87,7 +87,7 @@ class _AppReferenceState extends State { size: 22, ), text: l10n.aboutLinkLicense, - url: '${Constants.avesGithub}/blob/main/LICENSE', + urlString: '${Constants.avesGithub}/blob/main/LICENSE', ), LinkChip( leading: const Icon( diff --git a/lib/widgets/about/bug_report.dart b/lib/widgets/about/bug_report.dart index 20a08aaa9..2cdb16d6e 100644 --- a/lib/widgets/about/bug_report.dart +++ b/lib/widgets/about/bug_report.dart @@ -24,17 +24,18 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; class BugReport extends StatefulWidget { - const BugReport({Key? key}) : super(key: key); + const BugReport({super.key}); @override State createState() => _BugReportState(); } class _BugReportState extends State with FeedbackMixin { - final ScrollController _infoScrollController = ScrollController(); late Future _infoLoader; bool _showInstructions = false; + static final bugReportUri = Uri.parse('${Constants.avesGithub}/issues/new?labels=type%3Abug&template=bug_report.md'); + @override void initState() { super.initState(); @@ -87,28 +88,12 @@ class _BugReportState extends State with FeedbackMixin { constraints: const BoxConstraints(maxHeight: 100), margin: const EdgeInsets.symmetric(vertical: 8), clipBehavior: Clip.antiAlias, - child: Theme( - data: Theme.of(context).copyWith( - scrollbarTheme: const ScrollbarThemeData( - isAlwaysShown: true, - radius: Radius.circular(16), - crossAxisMargin: 6, - mainAxisMargin: 6, - interactive: true, - ), - ), - child: Scrollbar( - // when using `Scrollbar.isAlwaysShown`, a controller must be provided - // and used by both the `Scrollbar` and the `Scrollable`, but - // as of Flutter v2.8.1, `SelectableText` does not allow passing the `scrollController` - // so we wrap it in a `SingleChildScrollView` - controller: _infoScrollController, - child: SingleChildScrollView( - padding: const EdgeInsetsDirectional.only(start: 8, top: 4, end: 16, bottom: 4), - controller: _infoScrollController, - child: SelectableText(info), - ), - ), + child: SingleChildScrollView( + padding: const EdgeInsetsDirectional.only(start: 8, top: 4, end: 16, bottom: 4), + // to show a scroll bar, we would need to provide a scroll controller + // to both the `Scrollable` and the `Scrollbar`, but + // as of Flutter v3.0.0, `SelectableText` does not allow passing the `scrollController` + child: SelectableText(info), ), ); }, @@ -159,7 +144,6 @@ class _BugReportState extends State with FeedbackMixin { final packageInfo = await PackageInfo.fromPlatform(); final androidInfo = await DeviceInfoPlugin().androidInfo; final installer = await androidAppService.getAppInstaller(); - final hasPlayServices = await availability.hasPlayServices; final flavor = context.read().toString().split('.')[1]; return [ 'Aves version: ${packageInfo.version}-$flavor (Build ${packageInfo.buildNumber})', @@ -167,8 +151,8 @@ class _BugReportState extends State with FeedbackMixin { 'Android version: ${androidInfo.version.release} (SDK ${androidInfo.version.sdkInt})', 'Android build: ${androidInfo.display}', 'Device: ${androidInfo.manufacturer} ${androidInfo.model}', - 'Google Play services: ${hasPlayServices ? 'ready' : 'not available'}', - 'System locales: ${WidgetsBinding.instance!.window.locales.join(', ')}', + 'Mobile services: ${mobileServices.isServiceAvailable ? 'ready' : 'not available'}', + 'System locales: ${WidgetsBinding.instance.window.locales.join(', ')}', 'Aves locale: ${settings.locale ?? 'system'} -> ${settings.appliedLocale}', 'Installer: $installer', ].join('\n'); @@ -197,6 +181,8 @@ class _BugReportState extends State with FeedbackMixin { } Future _goToGithub() async { - await launch('${Constants.avesGithub}/issues/new?labels=type%3Abug&template=bug_report.md'); + if (await canLaunchUrl(bugReportUri)) { + await launchUrl(bugReportUri, mode: LaunchMode.externalApplication); + } } } diff --git a/lib/widgets/about/credits.dart b/lib/widgets/about/credits.dart index 945cfb00c..2e5595ea2 100644 --- a/lib/widgets/about/credits.dart +++ b/lib/widgets/about/credits.dart @@ -5,7 +5,7 @@ import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; class AboutCredits extends StatelessWidget { - const AboutCredits({Key? key}) : super(key: key); + const AboutCredits({super.key}); static const translators = { 'Bahasa Indonesia': 'MeFinity', @@ -40,7 +40,7 @@ class AboutCredits extends StatelessWidget { const WidgetSpan( child: LinkChip( text: 'World Atlas', - url: 'https://github.com/topojson/world-atlas', + urlString: 'https://github.com/topojson/world-atlas', textStyle: TextStyle(fontWeight: FontWeight.bold), ), alignment: PlaceholderAlignment.middle, diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index 103d8c90d..e6193e7c1 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -11,7 +11,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class Licenses extends StatefulWidget { - const Licenses({Key? key}) : super(key: key); + const Licenses({super.key}); @override State createState() => _LicensesState(); @@ -121,9 +121,9 @@ class LicenseRow extends StatelessWidget { final Dependency package; const LicenseRow({ - Key? key, + super.key, required this.package, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -138,14 +138,14 @@ class LicenseRow extends StatelessWidget { children: [ LinkChip( text: package.name, - url: package.sourceUrl, + urlString: package.sourceUrl, textStyle: const TextStyle(fontWeight: FontWeight.bold), ), Padding( padding: const EdgeInsetsDirectional.only(start: 16), child: LinkChip( text: package.license, - url: package.licenseUrl, + urlString: package.licenseUrl, color: subColor, ), ), diff --git a/lib/widgets/about/policy_page.dart b/lib/widgets/about/policy_page.dart index 534f3f7dc..ea4a1d113 100644 --- a/lib/widgets/about/policy_page.dart +++ b/lib/widgets/about/policy_page.dart @@ -6,9 +6,7 @@ import 'package:flutter/services.dart'; class PolicyPage extends StatefulWidget { static const routeName = '/about/policy'; - const PolicyPage({ - Key? key, - }) : super(key: key); + const PolicyPage({super.key}); @override State createState() => _PolicyPageState(); diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 46adbc61a..2ea40a119 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -42,10 +42,16 @@ import 'package:tuple/tuple.dart'; class AvesApp extends StatefulWidget { final AppFlavor flavor; + static final GlobalKey navigatorKey = GlobalKey(debugLabel: 'app-navigator'); + + // do not monitor all `ModalRoute`s, which would include popup menus, + // so that we can react to fullscreen `PageRoute`s only + static final RouteObserver pageRouteObserver = RouteObserver(); + const AvesApp({ - Key? key, + super.key, required this.flavor, - }) : super(key: key); + }); @override State createState() => _AvesAppState(); @@ -60,12 +66,11 @@ class _AvesAppState extends State with WidgetsBindingObserver { // observers are not registered when using the same list object with different items // the list itself needs to be reassigned - List _navigatorObservers = []; + List _navigatorObservers = [AvesApp.pageRouteObserver]; final EventChannel _mediaStoreChangeChannel = const EventChannel('deckers.thibault/aves/media_store_change'); final EventChannel _newIntentChannel = const EventChannel('deckers.thibault/aves/intent'); final EventChannel _analysisCompletionChannel = const EventChannel('deckers.thibault/aves/analysis_events'); final EventChannel _errorChannel = const EventChannel('deckers.thibault/aves/error'); - final GlobalKey _navigatorKey = GlobalKey(debugLabel: 'app-navigator'); Widget getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage(); @@ -78,7 +83,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { _newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?)); _analysisCompletionChannel.receiveBroadcastStream().listen((event) => _onAnalysisCompletion()); _errorChannel.receiveBroadcastStream().listen((event) => _onError(event as String?)); - WidgetsBinding.instance!.addObserver(this); + WidgetsBinding.instance.addObserver(this); } @override @@ -115,31 +120,32 @@ class _AvesAppState extends State with WidgetsBindingObserver { final settingsLocale = s.item1; final areAnimationsEnabled = s.item2; final themeBrightness = s.item3; - return MaterialApp( - navigatorKey: _navigatorKey, - home: home, - navigatorObservers: _navigatorObservers, - builder: (context, child) { + + final pageTransitionsTheme = areAnimationsEnabled // Flutter has various page transition implementations for Android: // - `FadeUpwardsPageTransitionsBuilder` on Oreo / API 27 and below // - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28 - // - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above - // As of Flutter v2.8.1, `FadeUpwardsPageTransitionsBuilder` is the default, regardless of versions. - // In practice, `ZoomPageTransitionsBuilder` feels unstable when transitioning from Album to Collection. - if (!areAnimationsEnabled) { - child = Theme( - data: Theme.of(context).copyWith( - // strip page transitions used by `MaterialPageRoute` - pageTransitionsTheme: DirectPageTransitionsTheme(), - ), - child: child!, - ); - } - return AvesColorsProvider( + // - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.0.0) + ? const PageTransitionsTheme( + builders: { + TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), + }, + ) + // strip page transitions used by `MaterialPageRoute` + : const DirectPageTransitionsTheme(); + + return MaterialApp( + navigatorKey: AvesApp.navigatorKey, + home: home, + navigatorObservers: _navigatorObservers, + builder: (context, child) => AvesColorsProvider( + child: Theme( + data: Theme.of(context).copyWith( + pageTransitionsTheme: pageTransitionsTheme, + ), child: child!, - ); - // return child!; - }, + ), + ), onGenerateTitle: (context) => context.l10n.appName, theme: Themes.lightTheme, darkTheme: themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme : Themes.darkTheme, @@ -147,6 +153,8 @@ class _AvesAppState extends State with WidgetsBindingObserver { locale: settingsLocale, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, + // TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906 + scrollBehavior: StretchMaterialScrollBehavior(), ); }, ); @@ -183,7 +191,8 @@ class _AvesAppState extends State with WidgetsBindingObserver { case AppLifecycleState.inactive: switch (appModeNotifier.value) { case AppMode.main: - case AppMode.pickMediaExternal: + case AppMode.pickSingleMediaExternal: + case AppMode.pickMultipleMediaExternal: _saveTopEntries(); break; case AppMode.pickMediaInternal: @@ -223,6 +232,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { final stopwatch = Stopwatch()..start(); await device.init(); + await mobileServices.init(); await settings.init(monitorPlatformSettings: true); settings.isRotationLocked = await windowService.isRotationLocked(); settings.areAnimationsRemoved = await AccessibilityService.areAnimationsRemoved(); @@ -271,18 +281,18 @@ class _AvesAppState extends State with WidgetsBindingObserver { FlutterError.onError = reportService.recordFlutterError; final now = DateTime.now(); - final hasPlayServices = await availability.hasPlayServices; await reportService.setCustomKeys({ 'build_mode': kReleaseMode ? 'release' : kProfileMode ? 'profile' : 'debug', - 'has_play_services': hasPlayServices, - 'locales': WidgetsBinding.instance!.window.locales.join(', '), + 'has_mobile_services': mobileServices.isServiceAvailable, + 'locales': WidgetsBinding.instance.window.locales.join(', '), 'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})', }); _navigatorObservers = [ + AvesApp.pageRouteObserver, ReportingRouteTracker(), ]; } @@ -294,7 +304,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { if (appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) return; reportService.log('New intent'); - _navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute( + AvesApp.navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute( settings: const RouteSettings(name: HomePage.routeName), builder: (_) => getFirstPage(intentData: intentData), )); @@ -324,3 +334,13 @@ class _AvesAppState extends State with WidgetsBindingObserver { void _onError(String? error) => reportService.recordError(error, null); } + +class StretchMaterialScrollBehavior extends MaterialScrollBehavior { + @override + Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) { + return StretchingOverscrollIndicator( + axisDirection: details.direction, + child: child, + ); + } +} diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 1a4860ef0..7aea5e78c 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_set_actions.dart'; @@ -18,13 +19,13 @@ import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/collection/filter_bar.dart'; import 'package:aves/widgets/collection/query_bar.dart'; -import 'package:aves/widgets/common/animated_icons_fix.dart'; import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/favourite_toggler.dart'; -import 'package:aves/widgets/common/sliver_app_bar_title.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/search/search_delegate.dart'; import 'package:flutter/material.dart'; @@ -37,22 +38,23 @@ class CollectionAppBar extends StatefulWidget { final CollectionLens collection; const CollectionAppBar({ - Key? key, + super.key, required this.appBarHeightNotifier, required this.collection, - }) : super(key: key); + }); @override State createState() => _CollectionAppBarState(); } -class _CollectionAppBarState extends State with SingleTickerProviderStateMixin { +class _CollectionAppBarState extends State with SingleTickerProviderStateMixin, WidgetsBindingObserver { final List _subscriptions = []; final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate(); late AnimationController _browseToSelectAnimation; final ValueNotifier _isSelectingNotifier = ValueNotifier(false); final FocusNode _queryBarFocusNode = FocusNode(); late final Listenable _queryFocusRequestNotifier; + double _statusBarHeight = 0; CollectionLens get collection => widget.collection; @@ -77,7 +79,11 @@ class _CollectionAppBarState extends State with SingleTickerPr ); _isSelectingNotifier.addListener(_onActivityChange); _registerWidget(widget); - WidgetsBinding.instance!.addPostFrameCallback((_) => _onFilterChanged()); + WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateStatusBarHeight(); + _onFilterChanged(); + }); } @override @@ -96,6 +102,7 @@ class _CollectionAppBarState extends State with SingleTickerPr _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); + WidgetsBinding.instance.removeObserver(this); super.dispose(); } @@ -107,6 +114,11 @@ class _CollectionAppBarState extends State with SingleTickerPr widget.collection.filterChangeNotifier.removeListener(_onFilterChanged); } + @override + void didChangeMetrics() { + _updateStatusBarHeight(); + } + @override Widget build(BuildContext context) { final appMode = context.watch>().value; @@ -122,15 +134,16 @@ class _CollectionAppBarState extends State with SingleTickerPr builder: (context, queryEnabled, child) { return Selector>( selector: (context, s) => s.collectionBrowsingQuickActions, - builder: (context, _, child) => SliverAppBar( - leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null, - title: SliverAppBarTitleWrapper( - child: _buildAppBarTitle(isSelecting), - ), - actions: _buildActions(selection), - bottom: PreferredSize( - preferredSize: Size.fromHeight(appBarBottomHeight), - child: Column( + builder: (context, _, child) { + return AvesAppBar( + contentHeight: appBarContentHeight, + leading: _buildAppBarLeading( + hasDrawer: appMode.hasDrawer, + isSelecting: isSelecting, + ), + title: _buildAppBarTitle(isSelecting), + actions: _buildActions(selection), + bottom: Column( children: [ if (showFilterBar) FilterBar( @@ -145,10 +158,9 @@ class _CollectionAppBarState extends State with SingleTickerPr ) ], ), - ), - titleSpacing: 0, - floating: true, - ), + transitionKey: isSelecting, + ); + }, ); }, ); @@ -156,12 +168,16 @@ class _CollectionAppBarState extends State with SingleTickerPr ); } - double get appBarBottomHeight { + double get appBarContentHeight { final hasQuery = context.read().enabled; - return (showFilterBar ? FilterBar.preferredHeight : .0) + (hasQuery ? EntryQueryBar.preferredHeight : .0); + return kToolbarHeight + (showFilterBar ? FilterBar.preferredHeight : .0) + (hasQuery ? EntryQueryBar.preferredHeight : .0); } - Widget _buildAppBarLeading(bool isSelecting) { + Widget _buildAppBarLeading({required bool hasDrawer, required bool isSelecting}) { + if (!hasDrawer) { + return const CloseButton(); + } + VoidCallback? onPressed; String? tooltip; if (isSelecting) { @@ -174,9 +190,8 @@ class _CollectionAppBarState extends State with SingleTickerPr return IconButton( // key is expected by test driver key: const Key('appbar-leading-button'), - // TODO TLAD [rtl] replace to regular `AnimatedIcon` when this is fixed: https://github.com/flutter/flutter/issues/60521 - icon: AnimatedIconFixIssue60521( - icon: AnimatedIconsFixIssue60521.menu_arrow, + icon: AnimatedIcon( + icon: AnimatedIcons.menu_arrow, progress: _browseToSelectAnimation, ), onPressed: onPressed, @@ -190,11 +205,21 @@ class _CollectionAppBarState extends State with SingleTickerPr if (isSelecting) { return Selector, int>( selector: (context, selection) => selection.selectedItems.length, - builder: (context, count, child) => Text(count == 0 ? l10n.collectionSelectPageTitle : l10n.itemCount(count)), + builder: (context, count, child) => Text( + count == 0 ? l10n.collectionSelectPageTitle : l10n.itemCount(count), + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), ); } else { final appMode = context.watch>().value; - Widget title = Text(appMode.isPickingMedia ? l10n.collectionPickPageTitle : (isTrash ? l10n.binPageTitle : l10n.collectionPageTitle)); + Widget title = Text( + appMode.isPickingMedia ? l10n.collectionPickPageTitle : (isTrash ? l10n.binPageTitle : l10n.collectionPageTitle), + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ); if (appMode == AppMode.main) { title = SourceStateAwareAppBarTitle( title: title, @@ -252,7 +277,7 @@ class _CollectionAppBarState extends State with SingleTickerPr ...(isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map( (action) => _toMenuItem(action, enabled: canApply(action), selection: selection), ), - if (isSelecting && !isTrash) + if (isSelecting && !isTrash && appMode == AppMode.main) PopupMenuItem( enabled: canApplyEditActions, padding: EdgeInsets.zero, @@ -420,7 +445,14 @@ class _CollectionAppBarState extends State with SingleTickerPr void _onQueryFocusRequest() => _queryBarFocusNode.requestFocus(); - void _updateAppBarHeight() => widget.appBarHeightNotifier.value = kToolbarHeight + appBarBottomHeight; + void _updateStatusBarHeight() { + _statusBarHeight = EdgeInsets.fromWindowPadding(window.padding, window.devicePixelRatio).top; + _updateAppBarHeight(); + } + + void _updateAppBarHeight() { + widget.appBarHeightNotifier.value = _statusBarHeight + AvesAppBar.appBarHeightForContentHeight(appBarContentHeight); + } Future _onActionSelected(EntrySetAction action) async { switch (action) { @@ -512,6 +544,7 @@ class _CollectionAppBarState extends State with SingleTickerPr context, SearchPageRoute( delegate: CollectionSearchDelegate( + searchFieldLabel: context.l10n.searchCollectionFieldHint, source: collection.source, parentCollection: collection, ), diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 64cd0cb8e..30a9c8da0 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -8,6 +8,7 @@ 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/enums.dart'; +import 'package:aves/model/source/section_keys.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -21,8 +22,10 @@ import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.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/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/selector.dart'; import 'package:aves/widgets/common/grid/sliver.dart'; import 'package:aves/widgets/common/grid/theme.dart'; @@ -31,8 +34,11 @@ 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/tile_extent_controller.dart'; +import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -45,9 +51,9 @@ class CollectionGrid extends StatefulWidget { static const double spacing = 2; const CollectionGrid({ - Key? key, + super.key, required this.settingsRouteKey, - }) : super(key: key); + }); @override State createState() => _CollectionGridState(); @@ -210,10 +216,9 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent child: scrollView, ); - final isMainMode = context.select, bool>((vn) => vn.value == AppMode.main); final selector = GridSelectionGestureDetector( scrollableKey: scrollableKey, - selectable: isMainMode, + selectable: context.select, bool>((v) => v.value.canSelectMedia), items: collection.sortedEntries, scrollController: scrollController, appBarHeightNotifier: appBarHeightNotifier, @@ -344,29 +349,45 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> { Widget _buildDraggableScrollView(ScrollView scrollView, CollectionLens collection) { return ValueListenableBuilder( valueListenable: widget.appBarHeightNotifier, - builder: (context, appBarHeight, child) => Selector( - selector: (context, mq) => mq.effectiveBottomPadding, - builder: (context, mqPaddingBottom, child) => DraggableScrollbar( - backgroundColor: Colors.white, - scrollThumbHeight: avesScrollThumbHeight, - scrollThumbBuilder: avesScrollThumbBuilder( - height: avesScrollThumbHeight, - backgroundColor: Colors.white, - ), - controller: widget.scrollController, - padding: EdgeInsets.only( - // padding to keep scroll thumb between app bar above and nav bar below - top: appBarHeight, - bottom: mqPaddingBottom, - ), - labelTextBuilder: (offsetY) => CollectionDraggableThumbLabel( - collection: collection, - offsetY: offsetY, - ), - child: scrollView, - ), - child: child, - ), + builder: (context, appBarHeight, child) { + return Selector( + selector: (context, mq) => mq.effectiveBottomPadding, + builder: (context, mqPaddingBottom, child) { + return Selector( + selector: (context, s) => s.showBottomNavigationBar, + builder: (context, showBottomNavigationBar, child) { + final navBarHeight = showBottomNavigationBar ? AppBottomNavBar.height : 0; + return Selector, List>( + selector: (context, layout) => layout.sectionLayouts, + builder: (context, sectionLayouts, child) { + return DraggableScrollbar( + backgroundColor: Colors.white, + scrollThumbSize: Size(avesScrollThumbWidth, avesScrollThumbHeight), + scrollThumbBuilder: avesScrollThumbBuilder( + height: avesScrollThumbHeight, + backgroundColor: Colors.white, + ), + controller: widget.scrollController, + crumbsBuilder: () => _getCrumbs(sectionLayouts), + padding: EdgeInsets.only( + // padding to keep scroll thumb between app bar above and nav bar below + top: appBarHeight, + bottom: navBarHeight + mqPaddingBottom, + ), + labelTextBuilder: (offsetY) => CollectionDraggableThumbLabel( + collection: collection, + offsetY: offsetY, + ), + crumbTextBuilder: (label) => DraggableCrumbLabel(label: label), + child: scrollView, + ); + }, + ); + }, + ); + }, + ); + }, ); } @@ -376,7 +397,12 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> { primary: true, // workaround to prevent scrolling the app bar away // when there is no content and we use `SliverFillRemaining` - physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : const SloppyScrollPhysics(parent: AlwaysScrollableScrollPhysics()), + physics: collection.isEmpty + ? const NeverScrollableScrollPhysics() + : SloppyScrollPhysics( + gestureSettings: context.select((mq) => mq.gestureSettings), + parent: const AlwaysScrollableScrollPhysics(), + ), cacheExtent: context.select((controller) => controller.effectiveExtentMax), slivers: [ appBar, @@ -386,6 +412,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> { child: _buildEmptyCollectionPlaceholder(collection), ) : const SectionedListSliver(), + const NavBarPaddingSliver(), const BottomPaddingSliver(), ], ); @@ -431,4 +458,65 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> { void _stopScrollMonitoringTimer() { _scrollMonitoringTimer?.cancel(); } + + Map _getCrumbs(List sectionLayouts) { + final crumbs = {}; + if (sectionLayouts.length <= 1) return crumbs; + + final maxOffset = sectionLayouts.last.maxOffset; + void addAlbums(CollectionLens collection, List sectionLayouts, Map crumbs) { + final source = collection.source; + sectionLayouts.forEach((section) { + final directory = (section.sectionKey as EntryAlbumSectionKey).directory; + if (directory != null) { + final label = source.getAlbumDisplayName(context, directory); + crumbs[section.minOffset / maxOffset] = label; + } + }); + } + + final collection = widget.collection; + switch (collection.sortFactor) { + case EntrySortFactor.date: + switch (collection.sectionFactor) { + case EntryGroupFactor.album: + addAlbums(collection, sectionLayouts, crumbs); + break; + case EntryGroupFactor.month: + case EntryGroupFactor.day: + final firstKey = sectionLayouts.first.sectionKey; + final lastKey = sectionLayouts.last.sectionKey; + if (firstKey is EntryDateSectionKey && lastKey is EntryDateSectionKey) { + final newest = firstKey.date; + final oldest = lastKey.date; + if (newest != null && oldest != null) { + final localeName = context.l10n.localeName; + final dateFormat = newest.difference(oldest).inDays > 365 ? DateFormat.y(localeName) : DateFormat.MMM(localeName); + String? lastLabel; + sectionLayouts.forEach((section) { + final date = (section.sectionKey as EntryDateSectionKey).date; + if (date != null) { + final label = dateFormat.format(date); + if (label != lastLabel) { + crumbs[section.minOffset / maxOffset] = label; + lastLabel = label; + } + } + }); + } + } + break; + case EntryGroupFactor.none: + break; + } + break; + case EntrySortFactor.name: + addAlbums(collection, sectionLayouts, crumbs); + break; + case EntrySortFactor.rating: + case EntrySortFactor.size: + break; + } + return crumbs; + } } diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 405fd7daa..d36b14166 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:aves/app_mode.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/query.dart'; @@ -9,14 +10,19 @@ import 'package:aves/model/selection.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/services/viewer_service.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/collection_grid.dart'; +import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/query_provider.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart'; -import 'package:aves/widgets/drawer/app_drawer.dart'; +import 'package:aves/widgets/navigation/drawer/app_drawer.dart'; +import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -30,11 +36,11 @@ class CollectionPage extends StatefulWidget { final bool Function(AvesEntry element)? highlightTest; const CollectionPage({ - Key? key, + super.key, required this.source, required this.filters, this.highlightTest, - }) : super(key: key); + }); @override State createState() => _CollectionPageState(); @@ -43,6 +49,7 @@ class CollectionPage extends StatefulWidget { class _CollectionPageState extends State { final List _subscriptions = []; late CollectionLens _collection; + final StreamController _draggableScrollBarEventStreamController = StreamController.broadcast(); @override void initState() { @@ -58,7 +65,7 @@ class _CollectionPageState extends State { _collection.removeFilter(TrashFilter.instance); } })); - WidgetsBinding.instance!.addPostFrameCallback((_) => _checkInitHighlight()); + WidgetsBinding.instance.addPostFrameCallback((_) => _checkInitHighlight()); } @override @@ -72,43 +79,87 @@ class _CollectionPageState extends State { @override Widget build(BuildContext context) { + final appMode = context.watch>().value; final liveFilter = _collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?; return MediaQueryDataProvider( - child: Scaffold( - body: SelectionProvider( - child: QueryProvider( - initialQuery: liveFilter?.query, - child: Builder( - builder: (context) => WillPopScope( - onWillPop: () { - final selection = context.read>(); - if (selection.isSelecting) { - selection.browse(); - return SynchronousFuture(false); - } - return SynchronousFuture(true); - }, - child: DoubleBackPopScope( - child: GestureAreaProtectorStack( - child: SafeArea( - bottom: false, - child: ChangeNotifierProvider.value( - value: _collection, - child: const CollectionGrid( - // key is expected by test driver - key: Key('collection-grid'), - settingsRouteKey: CollectionPage.routeName, + child: SelectionProvider( + child: Selector, bool>( + selector: (context, selection) => selection.selectedItems.isNotEmpty, + builder: (context, hasSelection, child) { + return Selector( + selector: (context, s) => s.showBottomNavigationBar, + builder: (context, showBottomNavigationBar, child) { + return NotificationListener( + onNotification: (notification) { + _draggableScrollBarEventStreamController.add(notification.event); + return false; + }, + child: Scaffold( + body: QueryProvider( + initialQuery: liveFilter?.query, + child: Builder( + builder: (context) => WillPopScope( + onWillPop: () { + final selection = context.read>(); + if (selection.isSelecting) { + selection.browse(); + return SynchronousFuture(false); + } + return SynchronousFuture(true); + }, + child: DoubleBackPopScope( + child: GestureAreaProtectorStack( + child: SafeArea( + top: false, + bottom: false, + child: ChangeNotifierProvider.value( + value: _collection, + child: const CollectionGrid( + // key is expected by test driver + key: Key('collection-grid'), + settingsRouteKey: CollectionPage.routeName, + ), + ), + ), + ), + ), ), ), ), + floatingActionButton: appMode == AppMode.pickMultipleMediaExternal && hasSelection + ? TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, + ), + child: FloatingActionButton( + tooltip: context.l10n.collectionPickPageTitle, + onPressed: () { + final items = context.read>().selectedItems; + final uris = items.map((entry) => entry.uri).toList(); + ViewerService.pick(uris); + }, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + child: const Icon(AIcons.apply), + ), + ) + : null, + drawer: AppDrawer(currentCollection: _collection), + bottomNavigationBar: showBottomNavigationBar + ? AppBottomNavBar( + events: _draggableScrollBarEventStreamController.stream, + currentCollection: _collection, + ) + : null, + resizeToAvoidBottomInset: false, + extendBody: true, ), - ), - ), - ), - ), + ); + }, + ); + }, ), - drawer: AppDrawer(currentCollection: _collection), - resizeToAvoidBottomInset: false, ), ); } diff --git a/lib/widgets/collection/draggable_thumb_label.dart b/lib/widgets/collection/draggable_thumb_label.dart index 90017bdc0..f759854b9 100644 --- a/lib/widgets/collection/draggable_thumb_label.dart +++ b/lib/widgets/collection/draggable_thumb_label.dart @@ -15,10 +15,10 @@ class CollectionDraggableThumbLabel extends StatelessWidget { final double offsetY; const CollectionDraggableThumbLabel({ - Key? key, + super.key, required this.collection, required this.offsetY, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 57225c150..b839a0843 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -27,6 +27,7 @@ import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/search/route.dart'; import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart'; import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; @@ -56,7 +57,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware case EntrySetAction.configureView: return true; case EntrySetAction.select: - return appMode.canSelect && !isSelecting; + return appMode.canSelectMedia && !isSelecting; case EntrySetAction.selectAll: return isSelecting && selectedItemCount < itemCount; case EntrySetAction.selectNone: @@ -69,7 +70,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware case EntrySetAction.addShortcut: return appMode == AppMode.main && !isSelecting && device.canPinShortcut && !isTrash; case EntrySetAction.emptyBin: - return isTrash; + return appMode == AppMode.main && isTrash; // browsing or selecting case EntrySetAction.map: case EntrySetAction.stats: @@ -566,6 +567,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware context, SearchPageRoute( delegate: CollectionSearchDelegate( + searchFieldLabel: context.l10n.searchCollectionFieldHint, source: collection.source, parentCollection: collection, ), diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart index 7d67a60ec..ee4129c49 100644 --- a/lib/widgets/collection/filter_bar.dart +++ b/lib/widgets/collection/filter_bar.dart @@ -12,12 +12,11 @@ class FilterBar extends StatefulWidget { final FilterCallback? onTap; FilterBar({ - Key? key, + super.key, required Set filters, required this.removable, this.onTap, - }) : filters = List.from(filters)..sort(), - super(key: key); + }) : filters = List.from(filters)..sort(); @override State createState() => _FilterBarState(); @@ -82,14 +81,13 @@ class _FilterBarState extends State { color: Colors.transparent, height: FilterBar.preferredHeight, child: NotificationListener( - // cancel notification bubbling so that the draggable scrollbar + // cancel notification bubbling so that the draggable scroll bar // does not misinterpret filter bar scrolling for collection scrolling onNotification: (notification) => true, child: AnimatedList( key: _animatedListKey, initialItemCount: widget.filters.length, scrollDirection: Axis.horizontal, - physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 4), itemBuilder: (context, index, animation) { if (index >= widget.filters.length) return const SizedBox(); diff --git a/lib/widgets/collection/grid/headers/album.dart b/lib/widgets/collection/grid/headers/album.dart index 5cf468649..77bd42aef 100644 --- a/lib/widgets/collection/grid/headers/album.dart +++ b/lib/widgets/collection/grid/headers/album.dart @@ -13,10 +13,10 @@ class AlbumSectionHeader extends StatelessWidget { final String? directory, albumName; const AlbumSectionHeader({ - Key? key, + super.key, required this.directory, required this.albumName, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/collection/grid/headers/any.dart b/lib/widgets/collection/grid/headers/any.dart index 7e1340005..d88929c68 100644 --- a/lib/widgets/collection/grid/headers/any.dart +++ b/lib/widgets/collection/grid/headers/any.dart @@ -17,11 +17,11 @@ class CollectionSectionHeader extends StatelessWidget { final double height; const CollectionSectionHeader({ - Key? key, + super.key, required this.collection, required this.sectionKey, required this.height, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -76,7 +76,7 @@ class CollectionSectionHeader extends StatelessWidget { } final textScaleFactor = MediaQuery.textScaleFactorOf(context); - headerExtent = max(headerExtent, SectionHeader.leadingDimension * textScaleFactor) + SectionHeader.padding.vertical; + headerExtent = max(headerExtent, SectionHeader.leadingSize.height * textScaleFactor) + SectionHeader.padding.vertical; return headerExtent; } } diff --git a/lib/widgets/collection/grid/headers/date.dart b/lib/widgets/collection/grid/headers/date.dart index abce6c9dd..033492287 100644 --- a/lib/widgets/collection/grid/headers/date.dart +++ b/lib/widgets/collection/grid/headers/date.dart @@ -9,9 +9,9 @@ class DaySectionHeader extends StatelessWidget { final DateTime? date; const DaySectionHeader({ - Key? key, + super.key, required this.date, - }) : super(key: key); + }); // Examples (en_US): // `MMMMd`: `April 15` @@ -56,9 +56,9 @@ class MonthSectionHeader extends StatelessWidget { final DateTime? date; const MonthSectionHeader({ - Key? key, + super.key, required this.date, - }) : super(key: key); + }); static String _formatDate(BuildContext context, DateTime? date) { final l10n = context.l10n; diff --git a/lib/widgets/collection/grid/headers/rating.dart b/lib/widgets/collection/grid/headers/rating.dart index 225e6923e..c6e0198f0 100644 --- a/lib/widgets/collection/grid/headers/rating.dart +++ b/lib/widgets/collection/grid/headers/rating.dart @@ -7,9 +7,9 @@ class RatingSectionHeader extends StatelessWidget { final int rating; const RatingSectionHeader({ - Key? key, + super.key, required this.rating, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/collection/grid/list_details.dart b/lib/widgets/collection/grid/list_details.dart index 608c9ea11..91c81c01f 100644 --- a/lib/widgets/collection/grid/list_details.dart +++ b/lib/widgets/collection/grid/list_details.dart @@ -14,9 +14,9 @@ class EntryListDetails extends StatelessWidget { final AvesEntry entry; const EntryListDetails({ - Key? key, + super.key, required this.entry, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/collection/grid/list_details_theme.dart b/lib/widgets/collection/grid/list_details_theme.dart index 0c3f16b43..e6bd16e8c 100644 --- a/lib/widgets/collection/grid/list_details_theme.dart +++ b/lib/widgets/collection/grid/list_details_theme.dart @@ -14,10 +14,10 @@ class EntryListDetailsTheme extends StatelessWidget { static const double titleDetailPadding = 6; const EntryListDetailsTheme({ - Key? key, + super.key, required this.extent, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/collection/grid/section_layout.dart b/lib/widgets/collection/grid/section_layout.dart index 56f055c43..bfced886b 100644 --- a/lib/widgets/collection/grid/section_layout.dart +++ b/lib/widgets/collection/grid/section_layout.dart @@ -1,6 +1,5 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; -import 'package:aves/model/source/enums.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'; @@ -10,29 +9,20 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider? isScrollingNotifier; const InteractiveTile({ - Key? key, + super.key, required this.collection, required this.entry, required this.thumbnailExtent, required this.tileLayout, this.isScrollingNotifier, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -43,8 +43,12 @@ class InteractiveTile extends StatelessWidget { _goToViewer(context); } break; - case AppMode.pickMediaExternal: - ViewerService.pick(entry.uri); + case AppMode.pickSingleMediaExternal: + ViewerService.pick([entry.uri]); + break; + case AppMode.pickMultipleMediaExternal: + final selection = context.read>(); + selection.toggleSelection(entry); break; case AppMode.pickMediaInternal: Navigator.pop(context, entry); @@ -101,7 +105,7 @@ class Tile extends StatelessWidget { final Object? Function()? heroTagger; const Tile({ - Key? key, + super.key, required this.entry, required this.thumbnailExtent, required this.tileLayout, @@ -109,7 +113,7 @@ class Tile extends StatelessWidget { this.highlightable = false, this.isScrollingNotifier, this.heroTagger, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/collection/query_bar.dart b/lib/widgets/collection/query_bar.dart index 2cc898ade..d1442b4c3 100644 --- a/lib/widgets/collection/query_bar.dart +++ b/lib/widgets/collection/query_bar.dart @@ -13,10 +13,10 @@ class EntryQueryBar extends StatefulWidget { static const preferredHeight = kToolbarHeight; const EntryQueryBar({ - Key? key, + super.key, required this.queryNotifier, required this.focusNode, - }) : super(key: key); + }); @override State createState() => _EntryQueryBarState(); diff --git a/lib/widgets/common/action_mixins/entry_storage.dart b/lib/widgets/common/action_mixins/entry_storage.dart index 11a0b11be..07f08265f 100644 --- a/lib/widgets/common/action_mixins/entry_storage.dart +++ b/lib/widgets/common/action_mixins/entry_storage.dart @@ -18,6 +18,7 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/enums.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; @@ -131,7 +132,9 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { onCancel: () => mediaFileService.cancelFileOp(opId), onDone: (processed) async { final successOps = processed.where((v) => v.success).toSet(); - final movedOps = successOps.where((v) => !v.skipped).toSet(); + + // move + final movedOps = successOps.where((v) => !v.skipped && !v.deleted).toSet(); final movedEntries = movedOps.map((v) => v.uri).map((uri) => entries.firstWhereOrNull((entry) => entry.uri == uri)).whereNotNull().toSet(); await source.updateAfterMove( todoEntries: entries, @@ -139,6 +142,12 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { destinationAlbums: destinationAlbums, movedOps: movedOps, ); + + // delete (when trying to move to bin obsolete entries) + final deletedOps = successOps.where((v) => v.deleted).toSet(); + final deletedUris = deletedOps.map((event) => event.uri).toSet(); + await source.removeEntries(deletedUris, includeTrash: true); + source.resumeMonitoring(); // cleanup @@ -161,18 +170,30 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { action = SnackBarAction( // TODO TLAD [l10n] key for "RESTORE" label: l10n.entryActionRestore.toUpperCase(), - onPressed: () => move( - context, - moveType: MoveType.fromBin, - entries: movedEntries, - hideShowAction: true, - ), + onPressed: () { + // local context may be deactivated when action is triggered after navigation + final context = AvesApp.navigatorKey.currentContext; + if (context != null) { + move( + context, + moveType: MoveType.fromBin, + entries: movedEntries, + hideShowAction: true, + ); + } + }, ); } } else if (!hideShowAction) { action = SnackBarAction( label: l10n.showButtonLabel, - onPressed: () => _showMovedItems(context, destinationAlbums, movedOps), + onPressed: () { + // local context may be deactivated when action is triggered after navigation + final context = AvesApp.navigatorKey.currentContext; + if (context != null) { + _showMovedItems(context, destinationAlbums, movedOps); + } + }, ); } } diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index ff483618c..c077cb420 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -7,6 +7,8 @@ import 'package:aves/model/settings/settings.dart'; 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/circle.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:flutter/foundation.dart'; @@ -36,10 +38,12 @@ mixin FeedbackMixin { // provide the messenger if feedback happens as the widget is disposed void showFeedbackWithMessenger(BuildContext context, ScaffoldMessengerState messenger, String message, [SnackBarAction? action]) { _getSnackBarDuration(action != null).then((duration) { + final start = DateTime.now(); final snackBarContent = _FeedbackMessage( message: message, progressColor: Theme.of(context).colorScheme.secondary, - duration: action != null ? duration : null, + start: start, + stop: action != null ? start.add(duration) : null, ); if (context.currentRouteName == EntryViewerPage.routeName) { @@ -55,23 +59,22 @@ mixin FeedbackMixin { (context) => SafeArea( child: Padding( padding: margin, - child: SnackBar( + child: OverlaySnackBar( content: snackBarContent, - animation: const AlwaysStoppedAnimation(1), action: action != null - ? SnackBarAction( - label: action.label, + ? TextButton( + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all(Theme.of(context).snackBarTheme.actionTextColor), + ), onPressed: () { - // the regular snack bar dismiss behavior is confused - // because it expects a `Scaffold` in context, - // so we manually dimiss the overlay entry notificationOverlayEntry?.dismiss(); action.onPressed(); }, + child: Text(action.label), ) : null, - duration: duration, dismissDirection: DismissDirection.horizontal, + onDismiss: () => notificationOverlayEntry?.dismiss(), ), ), ), @@ -101,6 +104,8 @@ mixin FeedbackMixin { return Duration(milliseconds: millis); case AccessibilityTimeout.appDefault: return appDefaultDuration; + case AccessibilityTimeout.s3: + return const Duration(seconds: 3); case AccessibilityTimeout.s10: return const Duration(seconds: 10); case AccessibilityTimeout.s30: @@ -144,12 +149,12 @@ class ReportOverlay extends StatefulWidget { final void Function(Set processed) onDone; const ReportOverlay({ - Key? key, + super.key, required this.opStream, required this.itemCount, required this.onCancel, required this.onDone, - }) : super(key: key); + }); @override State> createState() => _ReportOverlayState(); @@ -273,72 +278,83 @@ class _ReportOverlayState extends State> with SingleTickerPr class _FeedbackMessage extends StatefulWidget { final String message; - final Duration? duration; + final DateTime? start, stop; final Color progressColor; const _FeedbackMessage({ - Key? key, required this.message, required this.progressColor, - this.duration, - }) : super(key: key); + this.start, + this.stop, + }); @override State<_FeedbackMessage> createState() => _FeedbackMessageState(); } -class _FeedbackMessageState extends State<_FeedbackMessage> { - double _percent = 0; - late int _remainingSecs; - Timer? _timer; +class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerProviderStateMixin { + AnimationController? _animationController; + Animation? _remainingDurationMillis; + int? _totalDurationMillis; @override void initState() { super.initState(); - final duration = widget.duration; - if (duration != null) { - _remainingSecs = duration.inSeconds; - _timer = Timer.periodic(const Duration(seconds: 1), (_) { - setState(() => _remainingSecs--); - }); - WidgetsBinding.instance!.addPostFrameCallback((_) => setState(() => _percent = 1.0)); + final start = widget.start; + final stop = widget.stop; + if (start != null && stop != null) { + _totalDurationMillis = stop.difference(start).inMilliseconds; + final remainingDuration = stop.difference(DateTime.now()); + _animationController = AnimationController( + duration: remainingDuration, + vsync: this, + ); + _remainingDurationMillis = IntTween( + begin: remainingDuration.inMilliseconds, + end: 0, + ).animate(CurvedAnimation( + parent: _animationController!, + curve: Curves.linear, + )); + _animationController!.forward(); } } @override void dispose() { - _timer?.cancel(); + _animationController?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final text = Text(widget.message); - final duration = widget.duration; final theme = Theme.of(context); final contentTextStyle = theme.snackBarTheme.contentTextStyle ?? ThemeData(brightness: theme.brightness).textTheme.subtitle1; - return duration == null + return _remainingDurationMillis == null ? text : Row( children: [ Expanded(child: text), const SizedBox(width: 16), - CircularPercentIndicator( - percent: _percent, - lineWidth: 2, - radius: 16, - // progress color is provided by the caller, - // because we cannot use the app context theme here - backgroundColor: widget.progressColor, - progressColor: Colors.grey, - animation: true, - animationDuration: duration.inMilliseconds, - center: Text( - '$_remainingSecs', - style: contentTextStyle, - ), - animateFromLastPercent: true, - reverse: true, + AnimatedBuilder( + animation: _remainingDurationMillis!, + builder: (context, child) { + final remainingDurationMillis = _remainingDurationMillis!.value; + return CircularIndicator( + radius: 16, + lineWidth: 2, + percent: remainingDurationMillis / _totalDurationMillis!, + background: Colors.grey, + // progress color is provided by the caller, + // because we cannot use the app context theme here + foreground: widget.progressColor, + center: Text( + '${(remainingDurationMillis / 1000).ceil()}', + style: contentTextStyle, + ), + ); + }, ), ], ); @@ -349,9 +365,9 @@ class ActionFeedback extends StatefulWidget { final Widget? child; const ActionFeedback({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); @override State createState() => _ActionFeedbackState(); diff --git a/lib/widgets/common/action_mixins/overlay_snack_bar.dart b/lib/widgets/common/action_mixins/overlay_snack_bar.dart new file mode 100644 index 000000000..5d8f883fd --- /dev/null +++ b/lib/widgets/common/action_mixins/overlay_snack_bar.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; + +// adapted from Flutter `SnackBar` in `/material/snack_bar.dart` + +// As of Flutter v3.0.1, `SnackBar` is not customizable enough to add margin +// and ignore pointers in that area, so we use an overlay entry instead. +// This overlay entry is not below a `Scaffold` (which is expected by `SnackBar` +// and `SnackBarAction`), and is not dismissed the same way. +// This adaptation assumes the `SnackBarBehavior.floating` behavior and no animation. +class OverlaySnackBar extends StatelessWidget { + final Widget content; + final Widget? action; + final DismissDirection dismissDirection; + final VoidCallback onDismiss; + + const OverlaySnackBar({ + super.key, + required this.content, + required this.action, + required this.dismissDirection, + required this.onDismiss, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final snackBarTheme = theme.snackBarTheme; + final isThemeDark = theme.brightness == Brightness.dark; + final buttonColor = isThemeDark ? colorScheme.primary : colorScheme.secondary; + + final brightness = isThemeDark ? Brightness.light : Brightness.dark; + final themeBackgroundColor = isThemeDark ? colorScheme.onSurface : Color.alphaBlend(colorScheme.onSurface.withOpacity(0.80), colorScheme.surface); + final inverseTheme = theme.copyWith( + colorScheme: ColorScheme( + primary: colorScheme.onPrimary, + secondary: buttonColor, + surface: colorScheme.onSurface, + background: themeBackgroundColor, + error: colorScheme.onError, + onPrimary: colorScheme.primary, + onSecondary: colorScheme.secondary, + onSurface: colorScheme.surface, + onBackground: colorScheme.background, + onError: colorScheme.error, + brightness: brightness, + ), + ); + + final contentTextStyle = snackBarTheme.contentTextStyle ?? ThemeData(brightness: brightness).textTheme.subtitle1; + + const horizontalPadding = 16.0; + final padding = EdgeInsetsDirectional.only(start: horizontalPadding, end: action != null ? 0 : horizontalPadding); + const actionHorizontalMargin = horizontalPadding / 2; + const singleLineVerticalPadding = 14.0; + + Widget snackBar = Padding( + padding: padding, + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: singleLineVerticalPadding), + child: DefaultTextStyle( + style: contentTextStyle!, + child: content, + ), + ), + ), + if (action != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: actionHorizontalMargin), + child: TextButtonTheme( + data: TextButtonThemeData( + style: TextButton.styleFrom( + primary: buttonColor, + padding: const EdgeInsets.symmetric(horizontal: horizontalPadding), + ), + ), + child: action!, + ), + ), + ], + ), + ); + + final elevation = snackBarTheme.elevation ?? 6.0; + final backgroundColor = snackBarTheme.backgroundColor ?? inverseTheme.colorScheme.background; + final shape = snackBarTheme.shape ?? const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))); + + snackBar = Material( + shape: shape, + elevation: elevation, + color: backgroundColor, + child: Theme( + data: inverseTheme, + child: snackBar, + ), + ); + + const topMargin = 5.0; + const bottomMargin = 10.0; + const horizontalMargin = 15.0; + snackBar = Padding( + padding: const EdgeInsets.fromLTRB( + horizontalMargin, + topMargin, + horizontalMargin, + bottomMargin, + ), + child: snackBar, + ); + + snackBar = SafeArea( + top: false, + bottom: false, + child: snackBar, + ); + + snackBar = Semantics( + container: true, + liveRegion: true, + onDismiss: onDismiss, + child: Dismissible( + key: const Key('dismissible'), + direction: dismissDirection, + resizeDuration: null, + onDismissed: (direction) => onDismiss(), + child: snackBar, + ), + ); + + return snackBar; + } +} diff --git a/lib/widgets/common/animated_icons_fix.dart b/lib/widgets/common/animated_icons_fix.dart deleted file mode 100644 index 505364bc2..000000000 --- a/lib/widgets/common/animated_icons_fix.dart +++ /dev/null @@ -1,1315 +0,0 @@ -// TODO TLAD [rtl] remove the whole file when this is fixed: https://github.com/flutter/flutter/issues/60521 -// as of Flutter v2.8.1, mirrored animated icon is misplaced -// cf PR https://github.com/flutter/flutter/pull/93312 - -// ignore_for_file: constant_identifier_names, curly_braces_in_flow_control_structures, unnecessary_null_comparison -import 'dart:math' as math show pi; -import 'dart:ui' as ui show Paint, Path, Canvas; -import 'dart:ui' show lerpDouble; - -import 'package:flutter/widgets.dart'; - -abstract class AnimatedIconData { - /// Abstract const constructor. This constructor enables subclasses to provide - /// const constructors so that they can be used in const expressions. - const AnimatedIconData(); - - /// Whether this icon should be mirrored horizontally when text direction is - /// right-to-left. - /// - /// See also: - /// - /// * [TextDirection], which discusses concerns regarding reading direction - /// in Flutter. - /// * [Directionality], a widget which determines the ambient directionality. - bool get matchTextDirection; -} - -class _AnimatedIconData extends AnimatedIconData { - const _AnimatedIconData(this.size, this.paths, {this.matchTextDirection = false}); - - final Size size; - final List<_PathFrames> paths; - - @override - final bool matchTextDirection; -} - -class AnimatedIconFixIssue60521 extends StatelessWidget { - /// Creates an AnimatedIcon. - /// - /// The [progress] and [icon] arguments must not be null. - /// The [size] and [color] default to the value given by the current [IconTheme]. - const AnimatedIconFixIssue60521({ - Key? key, - required this.icon, - required this.progress, - this.color, - this.size, - this.semanticLabel, - this.textDirection, - }) : assert(progress != null), - assert(icon != null), - super(key: key); - - /// The animation progress for the animated icon. - /// - /// The value is clamped to be between 0 and 1. - /// - /// This determines the actual frame that is displayed. - final Animation progress; - - /// The color to use when drawing the icon. - /// - /// Defaults to the current [IconTheme] color, if any. - /// - /// The given color will be adjusted by the opacity of the current - /// [IconTheme], if any. - /// - /// In material apps, if there is a [Theme] without any [IconTheme]s - /// specified, icon colors default to white if the theme is dark - /// and black if the theme is light. - /// - /// If no [IconTheme] and no [Theme] is specified, icons will default to black. - /// - /// See [Theme] to set the current theme and [ThemeData.brightness] - /// for setting the current theme's brightness. - final Color? color; - - /// The size of the icon in logical pixels. - /// - /// Icons occupy a square with width and height equal to size. - /// - /// Defaults to the current [IconTheme] size. - final double? size; - - /// The icon to display. Available icons are listed in [AnimatedIcons]. - final AnimatedIconData icon; - - /// Semantic label for the icon. - /// - /// Announced in accessibility modes (e.g TalkBack/VoiceOver). - /// This label does not show in the UI. - /// - /// See also: - /// - /// * [SemanticsProperties.label], which is set to [semanticLabel] in the - /// underlying [Semantics] widget. - final String? semanticLabel; - - /// The text direction to use for rendering the icon. - /// - /// If this is null, the ambient [Directionality] is used instead. - /// - /// If the text direction is [TextDirection.rtl], the icon will be mirrored - /// horizontally (e.g back arrow will point right). - final TextDirection? textDirection; - - static ui.Path _pathFactory() => ui.Path(); - - @override - Widget build(BuildContext context) { - assert(debugCheckHasDirectionality(context)); - final _AnimatedIconData iconData = icon as _AnimatedIconData; - final IconThemeData iconTheme = IconTheme.of(context); - assert(iconTheme.isConcrete); - final double iconSize = size ?? iconTheme.size!; - final TextDirection textDirection = this.textDirection ?? Directionality.of(context); - final double iconOpacity = iconTheme.opacity!; - Color iconColor = color ?? iconTheme.color!; - if (iconOpacity != 1.0) iconColor = iconColor.withOpacity(iconColor.opacity * iconOpacity); - return Semantics( - label: semanticLabel, - child: CustomPaint( - size: Size(iconSize, iconSize), - painter: _AnimatedIconPainter( - paths: iconData.paths, - progress: progress, - color: iconColor, - scale: iconSize / iconData.size.width, - shouldMirror: textDirection == TextDirection.rtl && iconData.matchTextDirection, - uiPathFactory: _pathFactory, - ), - ), - ); - } -} - -typedef _UiPathFactory = ui.Path Function(); - -class _AnimatedIconPainter extends CustomPainter { - _AnimatedIconPainter({ - required this.paths, - required this.progress, - required this.color, - required this.scale, - required this.shouldMirror, - required this.uiPathFactory, - }) : super(repaint: progress); - - // This list is assumed to be immutable, changes to the contents of the list - // will not trigger a redraw as shouldRepaint will keep returning false. - final List<_PathFrames> paths; - final Animation progress; - final Color color; - final double scale; - - /// If this is true the image will be mirrored horizontally. - final bool shouldMirror; - final _UiPathFactory uiPathFactory; - - @override - void paint(ui.Canvas canvas, Size size) { - // The RenderCustomPaint render object performs canvas.save before invoking - // this and canvas.restore after, so we don't need to do it here. - if (shouldMirror) { - canvas.rotate(math.pi); - canvas.translate(-size.width, -size.height); - } - canvas.scale(scale, scale); - - final double clampedProgress = progress.value.clamp(0.0, 1.0); - for (final _PathFrames path in paths) path.paint(canvas, color, uiPathFactory, clampedProgress); - } - - @override - bool shouldRepaint(_AnimatedIconPainter oldDelegate) { - return oldDelegate.progress.value != progress.value || - oldDelegate.color != color - // We are comparing the paths list by reference, assuming the list is - // treated as immutable to be more efficient. - || - oldDelegate.paths != paths || - oldDelegate.scale != scale || - oldDelegate.uiPathFactory != uiPathFactory; - } - - @override - bool? hitTest(Offset position) => null; - - @override - bool shouldRebuildSemantics(CustomPainter oldDelegate) => false; - - @override - SemanticsBuilderCallback? get semanticsBuilder => null; -} - -class _PathFrames { - const _PathFrames({ - required this.commands, - required this.opacities, - }); - - final List<_PathCommand> commands; - final List opacities; - - void paint(ui.Canvas canvas, Color color, _UiPathFactory uiPathFactory, double progress) { - final double opacity = _interpolate(opacities, progress, lerpDouble)!; - final ui.Paint paint = ui.Paint() - ..style = PaintingStyle.fill - ..color = color.withOpacity(color.opacity * opacity); - final ui.Path path = uiPathFactory(); - for (final _PathCommand command in commands) command.apply(path, progress); - canvas.drawPath(path, paint); - } -} - -abstract class _PathCommand { - const _PathCommand(); - - /// Applies the path command to [path]. - /// - /// For example if the object is a [_PathMoveTo] command it will invoke - /// [Path.moveTo] on [path]. - void apply(ui.Path path, double progress); -} - -class _PathMoveTo extends _PathCommand { - const _PathMoveTo(this.points); - - final List points; - - @override - void apply(Path path, double progress) { - final Offset offset = _interpolate(points, progress, Offset.lerp)!; - path.moveTo(offset.dx, offset.dy); - } -} - -class _PathCubicTo extends _PathCommand { - const _PathCubicTo(this.controlPoints1, this.controlPoints2, this.targetPoints); - - final List controlPoints2; - final List controlPoints1; - final List targetPoints; - - @override - void apply(Path path, double progress) { - final Offset controlPoint1 = _interpolate(controlPoints1, progress, Offset.lerp)!; - final Offset controlPoint2 = _interpolate(controlPoints2, progress, Offset.lerp)!; - final Offset targetPoint = _interpolate(targetPoints, progress, Offset.lerp)!; - path.cubicTo( - controlPoint1.dx, - controlPoint1.dy, - controlPoint2.dx, - controlPoint2.dy, - targetPoint.dx, - targetPoint.dy, - ); - } -} - -// ignore: unused_element -class _PathLineTo extends _PathCommand { - const _PathLineTo(this.points); - - final List points; - - @override - void apply(Path path, double progress) { - final Offset point = _interpolate(points, progress, Offset.lerp)!; - path.lineTo(point.dx, point.dy); - } -} - -class _PathClose extends _PathCommand { - const _PathClose(); - - @override - void apply(Path path, double progress) { - path.close(); - } -} - -T _interpolate(List values, double progress, _Interpolator interpolator) { - assert(progress <= 1.0); - assert(progress >= 0.0); - if (values.length == 1) return values[0]; - final double targetIdx = lerpDouble(0, values.length - 1, progress)!; - final int lowIdx = targetIdx.floor(); - final int highIdx = targetIdx.ceil(); - final double t = targetIdx - lowIdx; - return interpolator(values[lowIdx], values[highIdx], t); -} - -typedef _Interpolator = T Function(T a, T b, double progress); - -abstract class AnimatedIconsFixIssue60521 { - static const AnimatedIconData menu_arrow = _AnimatedIconData( - Size(48.0, 48.0), - <_PathFrames>[ - _PathFrames( - opacities: [ - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - ], - commands: <_PathCommand>[ - _PathMoveTo( - [ - Offset(6.0, 26.0), - Offset(5.976562557689849, 25.638185989482512), - Offset(5.951781669661045, 24.367972149512962), - Offset(6.172793116155802, 21.823631861702058), - Offset(7.363587976838016, 17.665129222832853), - Offset(11.400806749308899, 11.800457098273661), - Offset(17.41878573585796, 8.03287301910486), - Offset(24.257523532175192, 6.996159828679087), - Offset(29.90338248135665, 8.291042849526), - Offset(33.76252909490214, 10.56619705548221), - Offset(36.23501636298456, 12.973675163618006), - Offset(37.77053540180521, 15.158665125787222), - Offset(38.70420448893307, 17.008159945496722), - Offset(39.260392038988186, 18.5104805430827), - Offset(39.58393261852967, 19.691668944482075), - Offset(39.766765502294305, 20.58840471665747), - Offset(39.866421084642994, 21.237322746452932), - Offset(39.91802804639694, 21.671102155152063), - Offset(39.94204075298555, 21.917555098992118), - Offset(39.94920417650143, 21.999827480806236), - Offset(39.94921875, 22.0), - ], - ), - _PathCubicTo( - [ - Offset(6.0, 26.0), - Offset(5.976562557689849, 25.638185989482512), - Offset(5.951781669661045, 24.367972149512962), - Offset(6.172793116155802, 21.823631861702058), - Offset(7.363587976838016, 17.665129222832853), - Offset(11.400806749308899, 11.800457098273661), - Offset(17.41878573585796, 8.03287301910486), - Offset(24.257523532175192, 6.996159828679087), - Offset(29.90338248135665, 8.291042849526), - Offset(33.76252909490214, 10.56619705548221), - Offset(36.23501636298456, 12.973675163618006), - Offset(37.77053540180521, 15.158665125787222), - Offset(38.70420448893307, 17.008159945496722), - Offset(39.260392038988186, 18.5104805430827), - Offset(39.58393261852967, 19.691668944482075), - Offset(39.766765502294305, 20.58840471665747), - Offset(39.866421084642994, 21.237322746452932), - Offset(39.91802804639694, 21.671102155152063), - Offset(39.94204075298555, 21.917555098992118), - Offset(39.94920417650143, 21.999827480806236), - Offset(39.94921875, 22.0), - ], - [ - Offset(42.0, 26.0), - Offset(41.91421333157091, 26.360426629492423), - Offset(41.55655262500356, 27.60382930516768), - Offset(40.57766190556539, 29.99090297157744), - Offset(38.19401046368096, 33.57567286235671), - Offset(32.70215654116029, 37.756226919427284), - Offset(26.22621984436523, 39.26167875408963), - Offset(20.102351173097617, 38.04803275423973), - Offset(15.903199608216863, 35.25316524725598), - Offset(13.57741782841064, 32.27000071222682), - Offset(12.442030802775209, 29.665215617986277), - Offset(11.981806515947115, 27.560177578292762), - Offset(11.879421136842055, 25.918712565594948), - Offset(11.95091483982305, 24.66543021784112), - Offset(12.092167805674123, 23.72603017548901), - Offset(12.245452640806768, 23.03857447590349), - Offset(12.379956070248545, 22.554583229506296), - Offset(12.480582865035407, 22.237279988168645), - Offset(12.541514124262473, 22.059212079933666), - Offset(12.562455771803593, 22.000123717314214), - Offset(12.562499999999996, 22.000000000000004), - ], - [ - Offset(42.0, 26.0), - Offset(41.91421333157091, 26.360426629492423), - Offset(41.55655262500356, 27.60382930516768), - Offset(40.57766190556539, 29.99090297157744), - Offset(38.19401046368096, 33.57567286235671), - Offset(32.70215654116029, 37.756226919427284), - Offset(26.22621984436523, 39.26167875408963), - Offset(20.102351173097617, 38.04803275423973), - Offset(15.903199608216863, 35.25316524725598), - Offset(13.57741782841064, 32.27000071222682), - Offset(12.442030802775209, 29.665215617986277), - Offset(11.981806515947115, 27.560177578292762), - Offset(11.879421136842055, 25.918712565594948), - Offset(11.95091483982305, 24.66543021784112), - Offset(12.092167805674123, 23.72603017548901), - Offset(12.245452640806768, 23.03857447590349), - Offset(12.379956070248545, 22.554583229506296), - Offset(12.480582865035407, 22.237279988168645), - Offset(12.541514124262473, 22.059212079933666), - Offset(12.562455771803593, 22.000123717314214), - Offset(12.562499999999996, 22.000000000000004), - ], - ), - _PathCubicTo( - [ - Offset(42.0, 26.0), - Offset(41.91421333157091, 26.360426629492423), - Offset(41.55655262500356, 27.60382930516768), - Offset(40.57766190556539, 29.99090297157744), - Offset(38.19401046368096, 33.57567286235671), - Offset(32.70215654116029, 37.756226919427284), - Offset(26.22621984436523, 39.26167875408963), - Offset(20.102351173097617, 38.04803275423973), - Offset(15.903199608216863, 35.25316524725598), - Offset(13.57741782841064, 32.27000071222682), - Offset(12.442030802775209, 29.665215617986277), - Offset(11.981806515947115, 27.560177578292762), - Offset(11.879421136842055, 25.918712565594948), - Offset(11.95091483982305, 24.66543021784112), - Offset(12.092167805674123, 23.72603017548901), - Offset(12.245452640806768, 23.03857447590349), - Offset(12.379956070248545, 22.554583229506296), - Offset(12.480582865035407, 22.237279988168645), - Offset(12.541514124262473, 22.059212079933666), - Offset(12.562455771803593, 22.000123717314214), - Offset(12.562499999999996, 22.000000000000004), - ], - [ - Offset(42.0, 22.0), - Offset(41.99458528858859, 22.361234167441474), - Offset(41.91859127809106, 23.620246996030513), - Offset(41.501535596836376, 26.09905798461081), - Offset(40.02840620381446, 30.021099432452637), - Offset(35.79419835461124, 35.2186537827727), - Offset(30.076040790179817, 38.175916954629336), - Offset(24.067012730992623, 38.57855959743385), - Offset(19.453150566288006, 37.096490556388844), - Offset(16.506465839286186, 34.99409280868502), - Offset(14.73924581501028, 32.939784778587686), - Offset(13.715334530064114, 31.165018854170466), - Offset(13.140377980959201, 29.714761542791386), - Offset(12.83036672005031, 28.56755327976071), - Offset(12.672939622830032, 27.683643609921106), - Offset(12.600162038813565, 27.02281609043513), - Offset(12.571432188039635, 26.54999771317575), - Offset(12.56310619400641, 26.23642863509033), - Offset(12.562193301685781, 26.059158626029138), - Offset(12.562499038934627, 26.000123717080207), - Offset(12.562499999999996, 26.000000000000004), - ], - [ - Offset(42.0, 22.0), - Offset(41.99458528858859, 22.361234167441474), - Offset(41.91859127809106, 23.620246996030513), - Offset(41.501535596836376, 26.09905798461081), - Offset(40.02840620381446, 30.021099432452637), - Offset(35.79419835461124, 35.2186537827727), - Offset(30.076040790179817, 38.175916954629336), - Offset(24.067012730992623, 38.57855959743385), - Offset(19.453150566288006, 37.096490556388844), - Offset(16.506465839286186, 34.99409280868502), - Offset(14.73924581501028, 32.939784778587686), - Offset(13.715334530064114, 31.165018854170466), - Offset(13.140377980959201, 29.714761542791386), - Offset(12.83036672005031, 28.56755327976071), - Offset(12.672939622830032, 27.683643609921106), - Offset(12.600162038813565, 27.02281609043513), - Offset(12.571432188039635, 26.54999771317575), - Offset(12.56310619400641, 26.23642863509033), - Offset(12.562193301685781, 26.059158626029138), - Offset(12.562499038934627, 26.000123717080207), - Offset(12.562499999999996, 26.000000000000004), - ], - ), - _PathCubicTo( - [ - Offset(42.0, 22.0), - Offset(41.99458528858859, 22.361234167441474), - Offset(41.91859127809106, 23.620246996030513), - Offset(41.501535596836376, 26.09905798461081), - Offset(40.02840620381446, 30.021099432452637), - Offset(35.79419835461124, 35.2186537827727), - Offset(30.076040790179817, 38.175916954629336), - Offset(24.067012730992623, 38.57855959743385), - Offset(19.453150566288006, 37.096490556388844), - Offset(16.506465839286186, 34.99409280868502), - Offset(14.73924581501028, 32.939784778587686), - Offset(13.715334530064114, 31.165018854170466), - Offset(13.140377980959201, 29.714761542791386), - Offset(12.83036672005031, 28.56755327976071), - Offset(12.672939622830032, 27.683643609921106), - Offset(12.600162038813565, 27.02281609043513), - Offset(12.571432188039635, 26.54999771317575), - Offset(12.56310619400641, 26.23642863509033), - Offset(12.562193301685781, 26.059158626029138), - Offset(12.562499038934627, 26.000123717080207), - Offset(12.562499999999996, 26.000000000000004), - ], - [ - Offset(6.0, 22.0), - Offset(6.056934514707525, 21.63899352743156), - Offset(6.3138203227485405, 20.384389840375796), - Offset(7.096666807426793, 17.931786874735423), - Offset(9.197983716971518, 14.110555792928775), - Offset(14.492848562759846, 9.262883961619078), - Offset(21.26860668167255, 6.947111219644562), - Offset(28.222185090070198, 7.526686671873211), - Offset(33.453333439427794, 10.134368158658866), - Offset(36.69157710577769, 13.290289151940406), - Offset(38.53223137521963, 16.248244324219414), - Offset(39.50406341592221, 18.763506401664923), - Offset(39.965161333050226, 20.80420892269316), - Offset(40.139843919215444, 22.41260360500229), - Offset(40.164704435685586, 23.649282378914172), - Offset(40.1214749003011, 24.572646331189105), - Offset(40.057897202434084, 25.232737230122385), - Offset(40.00055137536795, 25.670250802073745), - Offset(39.96271993040885, 25.917501645087587), - Offset(39.949247443632466, 25.99982748057223), - Offset(39.94921875, 26.0), - ], - [ - Offset(6.0, 22.0), - Offset(6.056934514707525, 21.63899352743156), - Offset(6.3138203227485405, 20.384389840375796), - Offset(7.096666807426793, 17.931786874735423), - Offset(9.197983716971518, 14.110555792928775), - Offset(14.492848562759846, 9.262883961619078), - Offset(21.26860668167255, 6.947111219644562), - Offset(28.222185090070198, 7.526686671873211), - Offset(33.453333439427794, 10.134368158658866), - Offset(36.69157710577769, 13.290289151940406), - Offset(38.53223137521963, 16.248244324219414), - Offset(39.50406341592221, 18.763506401664923), - Offset(39.965161333050226, 20.80420892269316), - Offset(40.139843919215444, 22.41260360500229), - Offset(40.164704435685586, 23.649282378914172), - Offset(40.1214749003011, 24.572646331189105), - Offset(40.057897202434084, 25.232737230122385), - Offset(40.00055137536795, 25.670250802073745), - Offset(39.96271993040885, 25.917501645087587), - Offset(39.949247443632466, 25.99982748057223), - Offset(39.94921875, 26.0), - ], - ), - _PathCubicTo( - [ - Offset(6.0, 22.0), - Offset(6.056934514707525, 21.63899352743156), - Offset(6.3138203227485405, 20.384389840375796), - Offset(7.096666807426793, 17.931786874735423), - Offset(9.197983716971518, 14.110555792928775), - Offset(14.492848562759846, 9.262883961619078), - Offset(21.26860668167255, 6.947111219644562), - Offset(28.222185090070198, 7.526686671873211), - Offset(33.453333439427794, 10.134368158658866), - Offset(36.69157710577769, 13.290289151940406), - Offset(38.53223137521963, 16.248244324219414), - Offset(39.50406341592221, 18.763506401664923), - Offset(39.965161333050226, 20.80420892269316), - Offset(40.139843919215444, 22.41260360500229), - Offset(40.164704435685586, 23.649282378914172), - Offset(40.1214749003011, 24.572646331189105), - Offset(40.057897202434084, 25.232737230122385), - Offset(40.00055137536795, 25.670250802073745), - Offset(39.96271993040885, 25.917501645087587), - Offset(39.949247443632466, 25.99982748057223), - Offset(39.94921875, 26.0), - ], - [ - Offset(6.0, 26.0), - Offset(5.976562557689849, 25.638185989482512), - Offset(5.951781669661045, 24.367972149512962), - Offset(6.172793116155802, 21.823631861702058), - Offset(7.363587976838016, 17.665129222832853), - Offset(11.400806749308899, 11.800457098273661), - Offset(17.41878573585796, 8.03287301910486), - Offset(24.257523532175192, 6.996159828679087), - Offset(29.90338248135665, 8.291042849526), - Offset(33.76252909490214, 10.56619705548221), - Offset(36.23501636298456, 12.973675163618006), - Offset(37.77053540180521, 15.158665125787222), - Offset(38.70420448893307, 17.008159945496722), - Offset(39.260392038988186, 18.5104805430827), - Offset(39.58393261852967, 19.691668944482075), - Offset(39.766765502294305, 20.58840471665747), - Offset(39.866421084642994, 21.237322746452932), - Offset(39.91802804639694, 21.671102155152063), - Offset(39.94204075298555, 21.917555098992118), - Offset(39.94920417650143, 21.999827480806236), - Offset(39.94921875, 22.0), - ], - [ - Offset(6.0, 26.0), - Offset(5.976562557689849, 25.638185989482512), - Offset(5.951781669661045, 24.367972149512962), - Offset(6.172793116155802, 21.823631861702058), - Offset(7.363587976838016, 17.665129222832853), - Offset(11.400806749308899, 11.800457098273661), - Offset(17.41878573585796, 8.03287301910486), - Offset(24.257523532175192, 6.996159828679087), - Offset(29.90338248135665, 8.291042849526), - Offset(33.76252909490214, 10.56619705548221), - Offset(36.23501636298456, 12.973675163618006), - Offset(37.77053540180521, 15.158665125787222), - Offset(38.70420448893307, 17.008159945496722), - Offset(39.260392038988186, 18.5104805430827), - Offset(39.58393261852967, 19.691668944482075), - Offset(39.766765502294305, 20.58840471665747), - Offset(39.866421084642994, 21.237322746452932), - Offset(39.91802804639694, 21.671102155152063), - Offset(39.94204075298555, 21.917555098992118), - Offset(39.94920417650143, 21.999827480806236), - Offset(39.94921875, 22.0), - ], - ), - _PathClose(), - ], - ), - _PathFrames( - opacities: [ - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - ], - commands: <_PathCommand>[ - _PathMoveTo( - [ - Offset(6.0, 36.0), - Offset(5.8396336833594695, 35.66398057820908), - Offset(5.329309336374063, 34.47365089829387), - Offset(4.546341863759643, 32.03857491308836), - Offset(3.9472816617934896, 27.893335303194206), - Offset(4.788314785722232, 21.470485758169694), - Offset(7.406922551234356, 16.186721598040453), - Offset(10.987511722222681, 12.449414121983239), - Offset(14.290737577882037, 10.382465570533384), - Offset(16.84152025666389, 9.340052761292668), - Offset(18.753361861843203, 8.79207829497377), - Offset(20.19495897321279, 8.483469022255434), - Offset(21.293826339887335, 8.297708512391797), - Offset(22.135385178177998, 8.180000583359465), - Offset(22.776244370552647, 8.102975309903787), - Offset(23.25488929254563, 8.051973096906334), - Offset(23.598629725699347, 8.018606137477462), - Offset(23.827700643867974, 7.99783596371886), - Offset(23.95771797811348, 7.986559676107813), - Offset(24.001111438945117, 7.982878122631195), - Offset(24.001202429357242, 7.98287044589657), - ], - ), - _PathCubicTo( - [ - Offset(6.0, 36.0), - Offset(5.8396336833594695, 35.66398057820908), - Offset(5.329309336374063, 34.47365089829387), - Offset(4.546341863759643, 32.03857491308836), - Offset(3.9472816617934896, 27.893335303194206), - Offset(4.788314785722232, 21.470485758169694), - Offset(7.406922551234356, 16.186721598040453), - Offset(10.987511722222681, 12.449414121983239), - Offset(14.290737577882037, 10.382465570533384), - Offset(16.84152025666389, 9.340052761292668), - Offset(18.753361861843203, 8.79207829497377), - Offset(20.19495897321279, 8.483469022255434), - Offset(21.293826339887335, 8.297708512391797), - Offset(22.135385178177998, 8.180000583359465), - Offset(22.776244370552647, 8.102975309903787), - Offset(23.25488929254563, 8.051973096906334), - Offset(23.598629725699347, 8.018606137477462), - Offset(23.827700643867974, 7.99783596371886), - Offset(23.95771797811348, 7.986559676107813), - Offset(24.001111438945117, 7.982878122631195), - Offset(24.001202429357242, 7.98287044589657), - ], - [ - Offset(42.0, 36.0), - Offset(41.7493389152824, 36.20520796529164), - Offset(40.85819701033384, 36.89246335931071), - Offset(39.01294315759756, 38.1256246432051), - Offset(35.758514239960064, 39.76970128020763), - Offset(30.180134511403956, 41.28645636464381), - Offset(24.56603417073137, 41.32925393403815), - Offset(19.271926095830622, 39.91690773672663), - Offset(15.201959304751512, 37.5726832793895), - Offset(12.456295622648877, 35.01429311055303), - Offset(10.686459838185314, 32.608514843335385), - Offset(9.579921816288039, 30.502293804851334), - Offset(8.90802993167501, 28.734147272525124), - Offset(8.513791284564158, 27.294928344333726), - Offset(8.292240475325507, 26.156988797411067), - Offset(8.174465865426919, 25.287693028463128), - Offset(8.11616441641861, 24.655137447505503), - Offset(8.089821190085125, 24.230473791307258), - Offset(8.079382709319852, 23.988506993748523), - Offset(8.076631388780909, 23.907616552409003), - Offset(8.076626005900048, 23.907446869353766), - ], - [ - Offset(42.0, 36.0), - Offset(41.7493389152824, 36.20520796529164), - Offset(40.85819701033384, 36.89246335931071), - Offset(39.01294315759756, 38.1256246432051), - Offset(35.758514239960064, 39.76970128020763), - Offset(30.180134511403956, 41.28645636464381), - Offset(24.56603417073137, 41.32925393403815), - Offset(19.271926095830622, 39.91690773672663), - Offset(15.201959304751512, 37.5726832793895), - Offset(12.456295622648877, 35.01429311055303), - Offset(10.686459838185314, 32.608514843335385), - Offset(9.579921816288039, 30.502293804851334), - Offset(8.90802993167501, 28.734147272525124), - Offset(8.513791284564158, 27.294928344333726), - Offset(8.292240475325507, 26.156988797411067), - Offset(8.174465865426919, 25.287693028463128), - Offset(8.11616441641861, 24.655137447505503), - Offset(8.089821190085125, 24.230473791307258), - Offset(8.079382709319852, 23.988506993748523), - Offset(8.076631388780909, 23.907616552409003), - Offset(8.076626005900048, 23.907446869353766), - ], - ), - _PathCubicTo( - [ - Offset(42.0, 36.0), - Offset(41.7493389152824, 36.20520796529164), - Offset(40.85819701033384, 36.89246335931071), - Offset(39.01294315759756, 38.1256246432051), - Offset(35.758514239960064, 39.76970128020763), - Offset(30.180134511403956, 41.28645636464381), - Offset(24.56603417073137, 41.32925393403815), - Offset(19.271926095830622, 39.91690773672663), - Offset(15.201959304751512, 37.5726832793895), - Offset(12.456295622648877, 35.01429311055303), - Offset(10.686459838185314, 32.608514843335385), - Offset(9.579921816288039, 30.502293804851334), - Offset(8.90802993167501, 28.734147272525124), - Offset(8.513791284564158, 27.294928344333726), - Offset(8.292240475325507, 26.156988797411067), - Offset(8.174465865426919, 25.287693028463128), - Offset(8.11616441641861, 24.655137447505503), - Offset(8.089821190085125, 24.230473791307258), - Offset(8.079382709319852, 23.988506993748523), - Offset(8.076631388780909, 23.907616552409003), - Offset(8.076626005900048, 23.907446869353766), - ], - [ - Offset(42.0, 32.0), - Offset(41.803966700752746, 32.205577011286266), - Offset(41.104447603276626, 32.89996903899956), - Offset(39.64402995767152, 34.17517788052204), - Offset(37.031973302731046, 35.97545970343111), - Offset(32.44508133022271, 37.98012671725157), - Offset(27.6644042246058, 38.77327245743646), - Offset(22.963108117227325, 38.302914175295534), - Offset(19.18039906547299, 36.862333955479784), - Offset(16.509090720567585, 35.04434211490934), - Offset(14.703380298498667, 33.21759365821649), - Offset(13.512146444284534, 31.556733263561572), - Offset(12.740174664860898, 30.12862517729895), - Offset(12.248059307884624, 28.947244716051806), - Offset(11.939734974297815, 28.002595790430043), - Offset(11.750425410476474, 27.27521551305395), - Offset(11.637314290474384, 26.742992599694542), - Offset(11.572897732210654, 26.384358993735816), - Offset(11.54031155133882, 26.17955109507089), - Offset(11.530083003283234, 26.111009046369567), - Offset(11.530061897030713, 26.110865227715482), - ], - [ - Offset(42.0, 32.0), - Offset(41.803966700752746, 32.205577011286266), - Offset(41.104447603276626, 32.89996903899956), - Offset(39.64402995767152, 34.17517788052204), - Offset(37.031973302731046, 35.97545970343111), - Offset(32.44508133022271, 37.98012671725157), - Offset(27.6644042246058, 38.77327245743646), - Offset(22.963108117227325, 38.302914175295534), - Offset(19.18039906547299, 36.862333955479784), - Offset(16.509090720567585, 35.04434211490934), - Offset(14.703380298498667, 33.21759365821649), - Offset(13.512146444284534, 31.556733263561572), - Offset(12.740174664860898, 30.12862517729895), - Offset(12.248059307884624, 28.947244716051806), - Offset(11.939734974297815, 28.002595790430043), - Offset(11.750425410476474, 27.27521551305395), - Offset(11.637314290474384, 26.742992599694542), - Offset(11.572897732210654, 26.384358993735816), - Offset(11.54031155133882, 26.17955109507089), - Offset(11.530083003283234, 26.111009046369567), - Offset(11.530061897030713, 26.110865227715482), - ], - ), - _PathCubicTo( - [ - Offset(42.0, 32.0), - Offset(41.803966700752746, 32.205577011286266), - Offset(41.104447603276626, 32.89996903899956), - Offset(39.64402995767152, 34.17517788052204), - Offset(37.031973302731046, 35.97545970343111), - Offset(32.44508133022271, 37.98012671725157), - Offset(27.6644042246058, 38.77327245743646), - Offset(22.963108117227325, 38.302914175295534), - Offset(19.18039906547299, 36.862333955479784), - Offset(16.509090720567585, 35.04434211490934), - Offset(14.703380298498667, 33.21759365821649), - Offset(13.512146444284534, 31.556733263561572), - Offset(12.740174664860898, 30.12862517729895), - Offset(12.248059307884624, 28.947244716051806), - Offset(11.939734974297815, 28.002595790430043), - Offset(11.750425410476474, 27.27521551305395), - Offset(11.637314290474384, 26.742992599694542), - Offset(11.572897732210654, 26.384358993735816), - Offset(11.54031155133882, 26.17955109507089), - Offset(11.530083003283234, 26.111009046369567), - Offset(11.530061897030713, 26.110865227715482), - ], - [ - Offset(6.0, 32.0), - Offset(5.899914425897517, 31.66443482499171), - Offset(5.601001082666045, 30.482888615847468), - Offset(5.242005036683729, 28.09953280239226), - Offset(5.346316156571252, 24.145975901906155), - Offset(7.249241148069178, 18.317100047682345), - Offset(10.710823881370487, 13.931896549234073), - Offset(14.817117889097364, 11.294374466111893), - Offset(18.288493245756, 10.248489378687303), - Offset(20.784419638077317, 10.013509863155594), - Offset(22.541938014255397, 10.075312777589325), - Offset(23.798109358346892, 10.220508832423288), - Offset(24.71461203122786, 10.370924674281323), - Offset(25.392890381083, 10.501349297587215), - Offset(25.896277759611298, 10.60605174724228), - Offset(26.265268043339944, 10.685909272436422), - Offset(26.526795349038366, 10.74364670273436), - Offset(26.699555102368272, 10.782158496973931), - Offset(26.79709065296033, 10.80399872839147), - Offset(26.829561509459538, 10.811282301423006), - Offset(26.829629554119695, 10.811297570626497), - ], - [ - Offset(6.0, 32.0), - Offset(5.899914425897517, 31.66443482499171), - Offset(5.601001082666045, 30.482888615847468), - Offset(5.242005036683729, 28.09953280239226), - Offset(5.346316156571252, 24.145975901906155), - Offset(7.249241148069178, 18.317100047682345), - Offset(10.710823881370487, 13.931896549234073), - Offset(14.817117889097364, 11.294374466111893), - Offset(18.288493245756, 10.248489378687303), - Offset(20.784419638077317, 10.013509863155594), - Offset(22.541938014255397, 10.075312777589325), - Offset(23.798109358346892, 10.220508832423288), - Offset(24.71461203122786, 10.370924674281323), - Offset(25.392890381083, 10.501349297587215), - Offset(25.896277759611298, 10.60605174724228), - Offset(26.265268043339944, 10.685909272436422), - Offset(26.526795349038366, 10.74364670273436), - Offset(26.699555102368272, 10.782158496973931), - Offset(26.79709065296033, 10.80399872839147), - Offset(26.829561509459538, 10.811282301423006), - Offset(26.829629554119695, 10.811297570626497), - ], - ), - _PathCubicTo( - [ - Offset(6.0, 32.0), - Offset(5.899914425897517, 31.66443482499171), - Offset(5.601001082666045, 30.482888615847468), - Offset(5.242005036683729, 28.09953280239226), - Offset(5.346316156571252, 24.145975901906155), - Offset(7.249241148069178, 18.317100047682345), - Offset(10.710823881370487, 13.931896549234073), - Offset(14.817117889097364, 11.294374466111893), - Offset(18.288493245756, 10.248489378687303), - Offset(20.784419638077317, 10.013509863155594), - Offset(22.541938014255397, 10.075312777589325), - Offset(23.798109358346892, 10.220508832423288), - Offset(24.71461203122786, 10.370924674281323), - Offset(25.392890381083, 10.501349297587215), - Offset(25.896277759611298, 10.60605174724228), - Offset(26.265268043339944, 10.685909272436422), - Offset(26.526795349038366, 10.74364670273436), - Offset(26.699555102368272, 10.782158496973931), - Offset(26.79709065296033, 10.80399872839147), - Offset(26.829561509459538, 10.811282301423006), - Offset(26.829629554119695, 10.811297570626497), - ], - [ - Offset(6.0, 36.0), - Offset(5.839633683308566, 35.66398057820831), - Offset(5.329309336323984, 34.47365089829046), - Offset(4.546341863735712, 32.03857491308413), - Offset(3.947281661825336, 27.893335303206097), - Offset(4.788314785746671, 21.47048575818877), - Offset(7.406922551270995, 16.18672159809414), - Offset(10.98751172223972, 12.449414122039723), - Offset(14.290737577881032, 10.382465570503403), - Offset(16.841520256655304, 9.340052761342939), - Offset(18.753361861827802, 8.792078295019234), - Offset(20.194958973207576, 8.483469022266245), - Offset(21.293826339889407, 8.297708512388375), - Offset(22.13538517817335, 8.180000583365981), - Offset(22.776244370563283, 8.102975309890528), - Offset(23.25488929251534, 8.051973096940955), - Offset(23.598629725644848, 8.018606137536025), - Offset(23.82770064384222, 7.997835963745423), - Offset(23.957717978081078, 7.986559676140466), - Offset(24.001111438940168, 7.982878122636148), - Offset(24.001202429373503, 7.982870445880305), - ], - [ - Offset(6.0, 36.0), - Offset(5.839633683308566, 35.66398057820831), - Offset(5.329309336323984, 34.47365089829046), - Offset(4.546341863735712, 32.03857491308413), - Offset(3.947281661825336, 27.893335303206097), - Offset(4.788314785746671, 21.47048575818877), - Offset(7.406922551270995, 16.18672159809414), - Offset(10.98751172223972, 12.449414122039723), - Offset(14.290737577881032, 10.382465570503403), - Offset(16.841520256655304, 9.340052761342939), - Offset(18.753361861827802, 8.792078295019234), - Offset(20.194958973207576, 8.483469022266245), - Offset(21.293826339889407, 8.297708512388375), - Offset(22.13538517817335, 8.180000583365981), - Offset(22.776244370563283, 8.102975309890528), - Offset(23.25488929251534, 8.051973096940955), - Offset(23.598629725644848, 8.018606137536025), - Offset(23.82770064384222, 7.997835963745423), - Offset(23.957717978081078, 7.986559676140466), - Offset(24.001111438940168, 7.982878122636148), - Offset(24.001202429373503, 7.982870445880305), - ], - ), - _PathClose(), - ], - ), - _PathFrames( - opacities: [ - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - ], - commands: <_PathCommand>[ - _PathMoveTo( - [ - Offset(6.0, 16.0), - Offset(6.222470088677106, 15.614531066984553), - Offset(7.071161725316092, 14.306422712262563), - Offset(9.085869786142727, 11.907139949336411), - Offset(13.311519331212619, 8.711520321213257), - Offset(21.694206315186374, 6.462423500731354), - Offset(30.07031570748504, 8.471955170698632), - Offset(36.20036889900587, 14.155750775196541), - Offset(38.533897479983715, 20.76099122996903), - Offset(38.182626701431914, 26.194302454359914), - Offset(36.59711302702814, 30.110286603895076), - Offset(34.63761335058528, 32.76106836363335), - Offset(32.7272901891386, 34.4927008221791), - Offset(31.04869117038896, 35.596105690451935), - Offset(29.664526028757855, 36.28441549314729), - Offset(28.581655311555835, 36.70452225851578), - Offset(27.782897949107628, 36.95396775456513), - Offset(27.242531133855476, 37.09522522130338), - Offset(26.933380541033216, 37.166375518103024), - Offset(26.82984682779076, 37.188656481991416), - Offset(26.829629554103434, 37.18870242935725), - ], - ), - _PathCubicTo( - [ - Offset(6.0, 16.0), - Offset(6.222470088677106, 15.614531066984553), - Offset(7.071161725316092, 14.306422712262563), - Offset(9.085869786142727, 11.907139949336411), - Offset(13.311519331212619, 8.711520321213257), - Offset(21.694206315186374, 6.462423500731354), - Offset(30.07031570748504, 8.471955170698632), - Offset(36.20036889900587, 14.155750775196541), - Offset(38.533897479983715, 20.76099122996903), - Offset(38.182626701431914, 26.194302454359914), - Offset(36.59711302702814, 30.110286603895076), - Offset(34.63761335058528, 32.76106836363335), - Offset(32.7272901891386, 34.4927008221791), - Offset(31.04869117038896, 35.596105690451935), - Offset(29.664526028757855, 36.28441549314729), - Offset(28.581655311555835, 36.70452225851578), - Offset(27.782897949107628, 36.95396775456513), - Offset(27.242531133855476, 37.09522522130338), - Offset(26.933380541033216, 37.166375518103024), - Offset(26.82984682779076, 37.188656481991416), - Offset(26.829629554103434, 37.18870242935725), - ], - [ - Offset(42.0, 16.0), - Offset(42.119273441095075, 16.516374018071716), - Offset(42.428662704565184, 18.32937541467259), - Offset(42.54812490043565, 21.94159775950881), - Offset(41.3111285319893, 27.683594454682137), - Offset(36.06395079582478, 35.01020271691918), - Offset(28.59459512599702, 38.51093769070532), - Offset(21.239886122259133, 38.07233071493643), - Offset(16.251628495692138, 35.34156866251391), - Offset(13.527101819238178, 32.27103394597236), - Offset(12.16858814546228, 29.604397296366464), - Offset(11.548946515009288, 27.474331231158473), - Offset(11.311114637013635, 25.826563435488687), - Offset(11.262012546535352, 24.572239162454554), - Offset(11.298221100690522, 23.63118177535833), - Offset(11.364474416879979, 22.940254245947138), - Offset(11.431638843687892, 22.451805922237554), - Offset(11.485090012547001, 22.130328573710905), - Offset(11.518417313485447, 21.949395273355513), - Offset(11.530012405933167, 21.889264075838188), - Offset(11.53003696527787, 21.889138124802937), - ], - [ - Offset(42.0, 16.0), - Offset(42.119273441095075, 16.516374018071716), - Offset(42.428662704565184, 18.32937541467259), - Offset(42.54812490043565, 21.94159775950881), - Offset(41.3111285319893, 27.683594454682137), - Offset(36.06395079582478, 35.01020271691918), - Offset(28.59459512599702, 38.51093769070532), - Offset(21.239886122259133, 38.07233071493643), - Offset(16.251628495692138, 35.34156866251391), - Offset(13.527101819238178, 32.27103394597236), - Offset(12.16858814546228, 29.604397296366464), - Offset(11.548946515009288, 27.474331231158473), - Offset(11.311114637013635, 25.826563435488687), - Offset(11.262012546535352, 24.572239162454554), - Offset(11.298221100690522, 23.63118177535833), - Offset(11.364474416879979, 22.940254245947138), - Offset(11.431638843687892, 22.451805922237554), - Offset(11.485090012547001, 22.130328573710905), - Offset(11.518417313485447, 21.949395273355513), - Offset(11.530012405933167, 21.889264075838188), - Offset(11.53003696527787, 21.889138124802937), - ], - ), - _PathCubicTo( - [ - Offset(42.0, 16.0), - Offset(42.119273441095075, 16.516374018071716), - Offset(42.428662704565184, 18.32937541467259), - Offset(42.54812490043565, 21.94159775950881), - Offset(41.3111285319893, 27.683594454682137), - Offset(36.06395079582478, 35.01020271691918), - Offset(28.59459512599702, 38.51093769070532), - Offset(21.239886122259133, 38.07233071493643), - Offset(16.251628495692138, 35.34156866251391), - Offset(13.527101819238178, 32.27103394597236), - Offset(12.16858814546228, 29.604397296366464), - Offset(11.548946515009288, 27.474331231158473), - Offset(11.311114637013635, 25.826563435488687), - Offset(11.262012546535352, 24.572239162454554), - Offset(11.298221100690522, 23.63118177535833), - Offset(11.364474416879979, 22.940254245947138), - Offset(11.431638843687892, 22.451805922237554), - Offset(11.485090012547001, 22.130328573710905), - Offset(11.518417313485447, 21.949395273355513), - Offset(11.530012405933167, 21.889264075838188), - Offset(11.53003696527787, 21.889138124802937), - ], - [ - Offset(42.0, 12.0), - Offset(42.22538630246601, 12.517777761542249), - Offset(42.90619853384615, 14.357900907446863), - Offset(43.759884509852945, 18.128995147835514), - Offset(43.66585885175813, 24.44736028078141), - Offset(39.74861752085834, 33.43380529842439), - Offset(32.57188683977151, 39.07136996422343), - Offset(24.376857043988256, 40.600018479197814), - Offset(17.959269400168804, 39.004426856660785), - Offset(13.850567169499653, 36.311009998593796), - Offset(11.374155956344177, 33.58880277176081), - Offset(9.917496515696001, 31.204288894581083), - Offset(9.07498759074148, 29.236785710939074), - Offset(8.597571742452605, 27.666692096657314), - Offset(8.334783321442917, 26.44693980672826), - Offset(8.195874559699876, 25.52824222288586), - Offset(8.126295299747222, 24.866824239052814), - Offset(8.093843447379264, 24.426077640310794), - Offset(8.080338503727083, 24.17611706018137), - Offset(8.076619249177135, 24.092742069165425), - Offset(8.07661186374038, 24.09256727275783), - ], - [ - Offset(42.0, 12.0), - Offset(42.22538630246601, 12.517777761542249), - Offset(42.90619853384615, 14.357900907446863), - Offset(43.759884509852945, 18.128995147835514), - Offset(43.66585885175813, 24.44736028078141), - Offset(39.74861752085834, 33.43380529842439), - Offset(32.57188683977151, 39.07136996422343), - Offset(24.376857043988256, 40.600018479197814), - Offset(17.959269400168804, 39.004426856660785), - Offset(13.850567169499653, 36.311009998593796), - Offset(11.374155956344177, 33.58880277176081), - Offset(9.917496515696001, 31.204288894581083), - Offset(9.07498759074148, 29.236785710939074), - Offset(8.597571742452605, 27.666692096657314), - Offset(8.334783321442917, 26.44693980672826), - Offset(8.195874559699876, 25.52824222288586), - Offset(8.126295299747222, 24.866824239052814), - Offset(8.093843447379264, 24.426077640310794), - Offset(8.080338503727083, 24.17611706018137), - Offset(8.076619249177135, 24.092742069165425), - Offset(8.07661186374038, 24.09256727275783), - ], - ), - _PathCubicTo( - [ - Offset(42.0, 12.0), - Offset(42.22538630246601, 12.517777761542249), - Offset(42.90619853384615, 14.357900907446863), - Offset(43.759884509852945, 18.128995147835514), - Offset(43.66585885175813, 24.44736028078141), - Offset(39.74861752085834, 33.43380529842439), - Offset(32.57188683977151, 39.07136996422343), - Offset(24.376857043988256, 40.600018479197814), - Offset(17.959269400168804, 39.004426856660785), - Offset(13.850567169499653, 36.311009998593796), - Offset(11.374155956344177, 33.58880277176081), - Offset(9.917496515696001, 31.204288894581083), - Offset(9.07498759074148, 29.236785710939074), - Offset(8.597571742452605, 27.666692096657314), - Offset(8.334783321442917, 26.44693980672826), - Offset(8.195874559699876, 25.52824222288586), - Offset(8.126295299747222, 24.866824239052814), - Offset(8.093843447379264, 24.426077640310794), - Offset(8.080338503727083, 24.17611706018137), - Offset(8.076619249177135, 24.092742069165425), - Offset(8.07661186374038, 24.09256727275783), - ], - [ - Offset(6.0, 12.0), - Offset(6.3229312318803075, 11.61579282114921), - Offset(7.523361420980265, 10.332065476778915), - Offset(10.234818160108134, 8.075701885898315), - Offset(15.555284551985588, 5.400098023461183), - Offset(25.267103519984172, 4.663978182144188), - Offset(34.065497532306516, 8.668225867992323), - Offset(39.59155761731576, 16.27703318845691), - Offset(40.72409454498984, 24.108085016590273), - Offset(39.139841854472834, 30.0780814324673), - Offset(36.514293313228855, 34.10942912386185), - Offset(33.744815583253256, 36.6601595585975), - Offset(31.226861893018718, 38.20062678263231), - Offset(29.10189988007002, 39.09038725780428), - Offset(27.3951953205187, 39.57837027981981), - Offset(26.083922435637483, 39.82883505984612), - Offset(25.128742795932077, 39.94653528477588), - Offset(24.487982707377697, 39.99564983955995), - Offset(24.123290412440365, 40.013021521592925), - Offset(24.001457946431486, 40.017121849607435), - Offset(24.001202429333205, 40.017129554079396), - ], - [ - Offset(6.0, 12.0), - Offset(6.3229312318803075, 11.61579282114921), - Offset(7.523361420980265, 10.332065476778915), - Offset(10.234818160108134, 8.075701885898315), - Offset(15.555284551985588, 5.400098023461183), - Offset(25.267103519984172, 4.663978182144188), - Offset(34.065497532306516, 8.668225867992323), - Offset(39.59155761731576, 16.27703318845691), - Offset(40.72409454498984, 24.108085016590273), - Offset(39.139841854472834, 30.0780814324673), - Offset(36.514293313228855, 34.10942912386185), - Offset(33.744815583253256, 36.6601595585975), - Offset(31.226861893018718, 38.20062678263231), - Offset(29.10189988007002, 39.09038725780428), - Offset(27.3951953205187, 39.57837027981981), - Offset(26.083922435637483, 39.82883505984612), - Offset(25.128742795932077, 39.94653528477588), - Offset(24.487982707377697, 39.99564983955995), - Offset(24.123290412440365, 40.013021521592925), - Offset(24.001457946431486, 40.017121849607435), - Offset(24.001202429333205, 40.017129554079396), - ], - ), - _PathCubicTo( - [ - Offset(6.0, 12.0), - Offset(6.3229312318803075, 11.61579282114921), - Offset(7.523361420980265, 10.332065476778915), - Offset(10.234818160108134, 8.075701885898315), - Offset(15.555284551985588, 5.400098023461183), - Offset(25.267103519984172, 4.663978182144188), - Offset(34.065497532306516, 8.668225867992323), - Offset(39.59155761731576, 16.27703318845691), - Offset(40.72409454498984, 24.108085016590273), - Offset(39.139841854472834, 30.0780814324673), - Offset(36.514293313228855, 34.10942912386185), - Offset(33.744815583253256, 36.6601595585975), - Offset(31.226861893018718, 38.20062678263231), - Offset(29.10189988007002, 39.09038725780428), - Offset(27.3951953205187, 39.57837027981981), - Offset(26.083922435637483, 39.82883505984612), - Offset(25.128742795932077, 39.94653528477588), - Offset(24.487982707377697, 39.99564983955995), - Offset(24.123290412440365, 40.013021521592925), - Offset(24.001457946431486, 40.017121849607435), - Offset(24.001202429333205, 40.017129554079396), - ], - [ - Offset(6.0, 16.0), - Offset(6.22247008872931, 15.614531066985863), - Offset(7.071161725356028, 14.306422712267109), - Offset(9.085869786222908, 11.907139949360454), - Offset(13.311519331206826, 8.711520321209331), - Offset(21.69420631520211, 6.462423500762615), - Offset(30.070315707485825, 8.471955170682651), - Offset(36.20036889903345, 14.155750775152455), - Offset(38.53389748002304, 20.760991229943293), - Offset(38.18262670145813, 26.194302454353455), - Offset(36.597113027065134, 30.110286603895844), - Offset(34.63761335066132, 32.761068363650764), - Offset(32.72729018913396, 34.49270082217723), - Offset(31.048691170407302, 35.59610569046216), - Offset(29.66452602881138, 36.28441549318417), - Offset(28.58165531160348, 36.70452225855387), - Offset(27.78289794916673, 36.95396775461755), - Offset(27.24253113386635, 37.09522522131371), - Offset(26.933380541051008, 37.16637551812059), - Offset(26.829846827821875, 37.18865648202253), - Offset(26.829629554079393, 37.188702429333205), - ], - [ - Offset(6.0, 16.0), - Offset(6.22247008872931, 15.614531066985863), - Offset(7.071161725356028, 14.306422712267109), - Offset(9.085869786222908, 11.907139949360454), - Offset(13.311519331206826, 8.711520321209331), - Offset(21.69420631520211, 6.462423500762615), - Offset(30.070315707485825, 8.471955170682651), - Offset(36.20036889903345, 14.155750775152455), - Offset(38.53389748002304, 20.760991229943293), - Offset(38.18262670145813, 26.194302454353455), - Offset(36.597113027065134, 30.110286603895844), - Offset(34.63761335066132, 32.761068363650764), - Offset(32.72729018913396, 34.49270082217723), - Offset(31.048691170407302, 35.59610569046216), - Offset(29.66452602881138, 36.28441549318417), - Offset(28.58165531160348, 36.70452225855387), - Offset(27.78289794916673, 36.95396775461755), - Offset(27.24253113386635, 37.09522522131371), - Offset(26.933380541051008, 37.16637551812059), - Offset(26.829846827821875, 37.18865648202253), - Offset(26.829629554079393, 37.188702429333205), - ], - ), - _PathClose(), - ], - ), - ], - matchTextDirection: true, - ); -} diff --git a/lib/widgets/common/app_bar_subtitle.dart b/lib/widgets/common/app_bar_subtitle.dart index 9871ca7e8..2a1df005d 100644 --- a/lib/widgets/common/app_bar_subtitle.dart +++ b/lib/widgets/common/app_bar_subtitle.dart @@ -13,10 +13,10 @@ class SourceStateAwareAppBarTitle extends StatelessWidget { final CollectionSource source; const SourceStateAwareAppBarTitle({ - Key? key, + super.key, required this.title, required this.source, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -54,9 +54,9 @@ class SourceStateSubtitle extends StatelessWidget { final CollectionSource source; const SourceStateSubtitle({ - Key? key, + super.key, required this.source, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/app_bar_title.dart b/lib/widgets/common/app_bar_title.dart index 57101d04b..09a7897a4 100644 --- a/lib/widgets/common/app_bar_title.dart +++ b/lib/widgets/common/app_bar_title.dart @@ -5,10 +5,10 @@ class InteractiveAppBarTitle extends StatelessWidget { final Widget child; const InteractiveAppBarTitle({ - Key? key, + super.key, this.onTap, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -18,7 +18,6 @@ class InteractiveAppBarTitle extends StatelessWidget { // so that we can also detect taps around the title `Text` child: Container( alignment: AlignmentDirectional.centerStart, - padding: const EdgeInsets.symmetric(horizontal: NavigationToolbar.kMiddleSpacing), color: Colors.transparent, height: kToolbarHeight, child: child, diff --git a/lib/widgets/common/aves_highlight.dart b/lib/widgets/common/aves_highlight.dart index 9c47278c2..2f1894ce4 100644 --- a/lib/widgets/common/aves_highlight.dart +++ b/lib/widgets/common/aves_highlight.dart @@ -1,3 +1,4 @@ +// ignore_for_file: depend_on_referenced_packages import 'package:flutter/material.dart'; import 'package:highlight/highlight.dart' show highlight, Node; @@ -30,15 +31,14 @@ class AvesHighlightView extends StatelessWidget { final TextStyle? textStyle; AvesHighlightView({ - Key? key, + super.key, required String input, this.language, this.theme = const {}, this.padding, this.textStyle, int tabSize = 8, // TODO: https://github.com/flutter/flutter/issues/50087 - }) : source = input.replaceAll('\t', ' ' * tabSize), - super(key: key); + }) : source = input.replaceAll('\t', ' ' * tabSize); List _convert(List nodes) { final spans = []; diff --git a/lib/widgets/common/basic/circle.dart b/lib/widgets/common/basic/circle.dart new file mode 100644 index 000000000..d829c6993 --- /dev/null +++ b/lib/widgets/common/basic/circle.dart @@ -0,0 +1,109 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class CircularIndicator extends StatefulWidget { + final double radius, lineWidth, percent; + final Color background, foreground; + final Widget center; + + const CircularIndicator({ + super.key, + required this.radius, + required this.lineWidth, + required this.percent, + required this.background, + required this.foreground, + required this.center, + }); + + @override + State createState() => _CircularIndicatorState(); +} + +class _CircularIndicatorState extends State { + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: widget.radius * 2, + child: Stack( + alignment: Alignment.center, + children: [ + Circle( + radius: widget.radius, + lineWidth: widget.lineWidth, + percent: 1.0, + color: widget.background, + ), + Circle( + radius: widget.radius, + lineWidth: widget.lineWidth, + percent: widget.percent, + color: widget.foreground, + ), + widget.center, + ], + ), + ); + } +} + +class Circle extends StatelessWidget { + final double radius, lineWidth, percent; + final Color color; + + const Circle({ + super.key, + required this.radius, + required this.lineWidth, + required this.percent, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + size: Size.square(radius), + painter: _CirclePainter( + lineWidth: lineWidth, + radius: radius - lineWidth / 2, + color: color, + percent: percent, + ), + ); + } +} + +class _CirclePainter extends CustomPainter { + final double radius, lineWidth, percent; + final Color color; + + const _CirclePainter({ + required this.radius, + required this.lineWidth, + required this.percent, + required this.color, + }); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final paint = Paint() + ..style = PaintingStyle.stroke + ..color = color + ..strokeWidth = lineWidth; + + canvas.translate(center.dx, center.dy); + canvas.rotate(-pi / 2); + canvas.drawArc( + Rect.fromCircle(center: Offset.zero, radius: radius), + 0, + 2 * pi * percent, + false, + paint, + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/lib/widgets/common/basic/color_list_tile.dart b/lib/widgets/common/basic/color_list_tile.dart index 91860f690..588f06b42 100644 --- a/lib/widgets/common/basic/color_list_tile.dart +++ b/lib/widgets/common/basic/color_list_tile.dart @@ -12,11 +12,11 @@ class ColorListTile extends StatelessWidget { static const double radius = 16.0; const ColorListTile({ - Key? key, + super.key, required this.title, required this.value, required this.onChanged, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -51,9 +51,9 @@ class ColorPickerDialog extends StatefulWidget { final Color initialValue; const ColorPickerDialog({ - Key? key, + super.key, required this.initialValue, - }) : super(key: key); + }); @override State createState() => _ColorPickerDialogState(); diff --git a/lib/widgets/common/basic/draggable_scrollbar.dart b/lib/widgets/common/basic/draggable_scrollbar.dart index b3b0ea687..4e31017b7 100644 --- a/lib/widgets/common/basic/draggable_scrollbar.dart +++ b/lib/widgets/common/basic/draggable_scrollbar.dart @@ -24,7 +24,8 @@ typedef ScrollThumbBuilder = Widget Function( }); /// Build a Text widget using the current scroll offset -typedef LabelTextBuilder = Widget Function(double offsetY); +typedef OffsetLabelBuilder = Widget Function(double offsetY); +typedef TextLabelBuilder = Widget Function(String label); /// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged /// for quick navigation of the BoxScrollView. @@ -32,14 +33,15 @@ class DraggableScrollbar extends StatefulWidget { /// The background color of the label and thumb final Color backgroundColor; - /// The height of the scroll thumb - final double scrollThumbHeight; + final Map Function()? crumbsBuilder; + + final Size scrollThumbSize; /// A function that builds a thumb using the current configuration final ScrollThumbBuilder scrollThumbBuilder; /// The amount of padding that should surround the thumb - final EdgeInsets? padding; + final EdgeInsets padding; /// Determines how quickly the scrollbar will animate in and out final Duration scrollbarAnimationDuration; @@ -48,7 +50,9 @@ class DraggableScrollbar extends StatefulWidget { final Duration scrollbarTimeToFade; /// Build a Text widget from the current offset in the BoxScrollView - final LabelTextBuilder? labelTextBuilder; + final OffsetLabelBuilder labelTextBuilder; + + final TextLabelBuilder crumbTextBuilder; /// The ScrollController for the BoxScrollView final ScrollController controller; @@ -57,22 +61,25 @@ class DraggableScrollbar extends StatefulWidget { final ScrollView child; DraggableScrollbar({ - Key? key, + super.key, required this.backgroundColor, - required this.scrollThumbHeight, + required this.scrollThumbSize, required this.scrollThumbBuilder, required this.controller, - this.padding, + this.crumbsBuilder, + this.padding = EdgeInsets.zero, this.scrollbarAnimationDuration = const Duration(milliseconds: 300), this.scrollbarTimeToFade = const Duration(milliseconds: 1000), - this.labelTextBuilder, + required this.labelTextBuilder, + required this.crumbTextBuilder, required this.child, - }) : assert(child.scrollDirection == Axis.vertical), - super(key: key); + }) : assert(child.scrollDirection == Axis.vertical); @override State createState() => _DraggableScrollbarState(); + static const double labelThumbPadding = 16; + static Widget buildScrollThumbAndLabel({ required Widget scrollThumb, required Color backgroundColor, @@ -91,7 +98,7 @@ class DraggableScrollbar extends StatefulWidget { backgroundColor: backgroundColor, child: labelText, ), - const SizedBox(width: 24), + const SizedBox(width: labelThumbPadding), scrollThumb, ], ); @@ -108,11 +115,11 @@ class ScrollLabel extends StatelessWidget { final Widget child; const ScrollLabel({ - Key? key, + super.key, required this.child, required this.animation, required this.backgroundColor, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -141,6 +148,11 @@ class _DraggableScrollbarState extends State with TickerProv late AnimationController _labelAnimationController; late Animation _labelAnimation; Timer? _fadeoutTimer; + Map? _percentCrumbs; + final Map _viewportCrumbs = {}; + + static const double crumbPadding = 30; + static const double crumbMinViewportRatio = 4; @override void initState() { @@ -167,6 +179,15 @@ class _DraggableScrollbarState extends State with TickerProv ); } + @override + void didUpdateWidget(covariant DraggableScrollbar oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.crumbsBuilder != widget.crumbsBuilder) { + _percentCrumbs = null; + } + } + @override void dispose() { _thumbAnimationController.dispose(); @@ -177,7 +198,9 @@ class _DraggableScrollbarState extends State with TickerProv ScrollController get controller => widget.controller; - double get thumbMaxScrollExtent => context.size!.height - widget.scrollThumbHeight - (widget.padding?.vertical ?? 0.0); + double get scrollBarHeight => context.size!.height - widget.padding.vertical; + + double get thumbMaxScrollExtent => scrollBarHeight - widget.scrollThumbSize.height; double get thumbMinScrollExtent => 0.0; @@ -193,6 +216,29 @@ class _DraggableScrollbarState extends State with TickerProv RepaintBoundary( child: widget.child, ), + if (_isDragInProcess) + ..._viewportCrumbs.entries.map((kv) { + final offset = kv.key; + final label = kv.value; + return Positioned.directional( + textDirection: Directionality.of(context), + top: offset, + end: DraggableScrollbar.labelThumbPadding + widget.scrollThumbSize.width, + child: Padding( + padding: widget.padding, + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: widget.scrollThumbSize.height), + child: Center( + child: ScrollLabel( + animation: kAlwaysCompleteAnimation, + backgroundColor: widget.backgroundColor, + child: widget.crumbTextBuilder(label), + ), + ), + ), + ), + ); + }), RepaintBoundary( child: GestureDetector( onLongPressStart: (details) { @@ -212,16 +258,16 @@ class _DraggableScrollbarState extends State with TickerProv valueListenable: _thumbOffsetNotifier, builder: (context, thumbOffset, child) => Container( alignment: AlignmentDirectional.topEnd, - padding: EdgeInsets.only(top: thumbOffset) + (widget.padding ?? EdgeInsets.zero), + padding: EdgeInsets.only(top: thumbOffset) + widget.padding, child: widget.scrollThumbBuilder( widget.backgroundColor, _thumbAnimation, _labelAnimation, - widget.scrollThumbHeight, - labelText: (widget.labelTextBuilder != null && _isDragInProcess) + widget.scrollThumbSize.height, + labelText: _isDragInProcess ? ValueListenableBuilder( valueListenable: _viewOffsetNotifier, - builder: (context, viewOffset, child) => widget.labelTextBuilder!.call(viewOffset + thumbOffset), + builder: (context, viewOffset, child) => widget.labelTextBuilder(viewOffset + thumbOffset), ) : null, ), @@ -258,9 +304,11 @@ class _DraggableScrollbarState extends State with TickerProv } void _onVerticalDragStart() { + const DraggableScrollBarNotification(DraggableScrollBarEvent.dragStart).dispatch(context); _labelAnimationController.forward(); _fadeoutTimer?.cancel(); _showThumb(); + _updateViewportCrumbs(); setState(() => _isDragInProcess = true); } @@ -278,6 +326,7 @@ class _DraggableScrollbarState extends State with TickerProv } void _onVerticalDragEnd() { + const DraggableScrollBarNotification(DraggableScrollBarEvent.dragEnd).dispatch(context); _scheduleFadeout(); setState(() => _isDragInProcess = false); } @@ -296,6 +345,33 @@ class _DraggableScrollbarState extends State with TickerProv _fadeoutTimer = null; }); } + + void _updateViewportCrumbs() { + _viewportCrumbs.clear(); + final crumbsBuilder = widget.crumbsBuilder; + if (crumbsBuilder != null) { + final maxOffset = thumbMaxScrollExtent; + final position = controller.position; + if (position.maxScrollExtent / position.viewportDimension > crumbMinViewportRatio) { + double lastLabelOffset = -crumbPadding; + _percentCrumbs ??= crumbsBuilder(); + _percentCrumbs!.entries.forEach((kv) { + final percent = kv.key; + final label = kv.value; + final labelOffset = percent * maxOffset; + if (labelOffset >= lastLabelOffset + crumbPadding) { + lastLabelOffset = labelOffset; + _viewportCrumbs[labelOffset] = label; + } + }); + // hide lonesome crumb, whether it is because of a single section, + // or because multiple sections collapsed to a single crumb + if (_viewportCrumbs.length == 1) { + _viewportCrumbs.clear(); + } + } + } + } } ///This cut 2 lines in arrow shape @@ -341,10 +417,10 @@ class SlideFadeTransition extends StatelessWidget { final Widget child; const SlideFadeTransition({ - Key? key, + super.key, required this.animation, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -364,3 +440,12 @@ class SlideFadeTransition extends StatelessWidget { ); } } + +@immutable +class DraggableScrollBarNotification extends Notification { + final DraggableScrollBarEvent event; + + const DraggableScrollBarNotification(this.event); +} + +enum DraggableScrollBarEvent { dragStart, dragEnd } diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart index 4915aea22..ce6b182d8 100644 --- a/lib/widgets/common/basic/insets.dart +++ b/lib/widgets/common/basic/insets.dart @@ -7,7 +7,7 @@ import 'package:provider/provider.dart'; // - a vertically scrollable body. // It will prevent the body from scrolling when a user swipe from bottom to use Android Q style navigation gestures. class BottomGestureAreaProtector extends StatelessWidget { - const BottomGestureAreaProtector({Key? key}) : super(key: key); + const BottomGestureAreaProtector({super.key}); @override Widget build(BuildContext context) { @@ -27,7 +27,7 @@ class BottomGestureAreaProtector extends StatelessWidget { // It will prevent the body from scrolling when a user swipe from edges to use Android Q style navigation gestures. class SideGestureAreaProtector extends StatelessWidget { - const SideGestureAreaProtector({Key? key}) : super(key: key); + const SideGestureAreaProtector({super.key}); @override Widget build(BuildContext context) { @@ -63,9 +63,9 @@ class GestureAreaProtectorStack extends StatelessWidget { final Widget child; const GestureAreaProtectorStack({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -79,7 +79,7 @@ class GestureAreaProtectorStack extends StatelessWidget { } class BottomPaddingSliver extends StatelessWidget { - const BottomPaddingSliver({Key? key}) : super(key: key); + const BottomPaddingSliver({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/basic/link_chip.dart b/lib/widgets/common/basic/link_chip.dart index 63929a809..6e4ed0766 100644 --- a/lib/widgets/common/basic/link_chip.dart +++ b/lib/widgets/common/basic/link_chip.dart @@ -5,7 +5,7 @@ import 'package:url_launcher/url_launcher.dart'; class LinkChip extends StatelessWidget { final Widget? leading; final String text; - final String? url; + final String? urlString; final Color? color; final TextStyle? textStyle; final VoidCallback? onTap; @@ -13,26 +13,29 @@ class LinkChip extends StatelessWidget { static const borderRadius = BorderRadius.all(Radius.circular(8)); const LinkChip({ - Key? key, + super.key, this.leading, required this.text, - this.url, + this.urlString, this.color, this.textStyle, this.onTap, - }) : super(key: key); + }); @override Widget build(BuildContext context) { - final _url = url; + final _urlString = urlString; return DefaultTextStyle.merge( style: (textStyle ?? const TextStyle()).copyWith(color: color), child: InkWell( borderRadius: borderRadius, onTap: onTap ?? () async { - if (_url != null && await canLaunch(_url)) { - await launch(_url); + if (_urlString != null) { + final url = Uri.parse(_urlString); + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } } }, child: Padding( diff --git a/lib/widgets/common/basic/markdown_container.dart b/lib/widgets/common/basic/markdown_container.dart index 43a37964f..03feb2280 100644 --- a/lib/widgets/common/basic/markdown_container.dart +++ b/lib/widgets/common/basic/markdown_container.dart @@ -8,10 +8,10 @@ class MarkdownContainer extends StatelessWidget { final TextDirection? textDirection; const MarkdownContainer({ - Key? key, + super.key, required this.data, this.textDirection, - }) : super(key: key); + }); static const double maxWidth = 460; @@ -29,9 +29,9 @@ class MarkdownContainer extends StatelessWidget { borderRadius: const BorderRadius.all(Radius.circular(16)), child: Theme( data: Theme.of(context).copyWith( - scrollbarTheme: const ScrollbarThemeData( - isAlwaysShown: true, - radius: Radius.circular(16), + scrollbarTheme: ScrollbarThemeData( + thumbVisibility: MaterialStateProperty.all(true), + radius: const Radius.circular(16), crossAxisMargin: 6, mainAxisMargin: 16, interactive: true, @@ -44,8 +44,11 @@ class MarkdownContainer extends StatelessWidget { data: data, selectable: true, onTapLink: (text, href, title) async { - if (href != null && await canLaunch(href)) { - await launch(href); + if (href != null) { + final url = Uri.parse(href); + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } } }, shrinkWrap: true, diff --git a/lib/widgets/common/basic/menu.dart b/lib/widgets/common/basic/menu.dart index 8c568a77a..695e2c73e 100644 --- a/lib/widgets/common/basic/menu.dart +++ b/lib/widgets/common/basic/menu.dart @@ -7,10 +7,10 @@ class MenuRow extends StatelessWidget { final Widget? icon; const MenuRow({ - Key? key, + super.key, required this.text, this.icon, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -37,9 +37,9 @@ class MenuIconTheme extends StatelessWidget { final Widget child; const MenuIconTheme({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -60,12 +60,12 @@ class PopupMenuItemExpansionPanel extends StatefulWidget { final List> items; const PopupMenuItemExpansionPanel({ - Key? key, + super.key, this.enabled = true, required this.icon, required this.title, required this.items, - }) : super(key: key); + }); @override State> createState() => _PopupMenuItemExpansionPanelState(); diff --git a/lib/widgets/common/basic/multi_cross_fader.dart b/lib/widgets/common/basic/multi_cross_fader.dart index 95be7ff94..ba3d01c01 100644 --- a/lib/widgets/common/basic/multi_cross_fader.dart +++ b/lib/widgets/common/basic/multi_cross_fader.dart @@ -7,13 +7,13 @@ class MultiCrossFader extends StatefulWidget { final Widget child; const MultiCrossFader({ - Key? key, + super.key, required this.duration, this.fadeCurve = Curves.linear, this.sizeCurve = Curves.linear, this.alignment = Alignment.topCenter, required this.child, - }) : super(key: key); + }); @override State createState() => _MultiCrossFaderState(); diff --git a/lib/widgets/common/basic/outlined_text.dart b/lib/widgets/common/basic/outlined_text.dart index 12a4812c2..4b1e787d9 100644 --- a/lib/widgets/common/basic/outlined_text.dart +++ b/lib/widgets/common/basic/outlined_text.dart @@ -12,7 +12,7 @@ class OutlinedText extends StatelessWidget { static const widgetSpanAlignment = PlaceholderAlignment.middle; const OutlinedText({ - Key? key, + super.key, required this.textSpans, double? outlineWidth, Color? outlineColor, @@ -20,8 +20,7 @@ class OutlinedText extends StatelessWidget { this.textAlign, }) : outlineWidth = outlineWidth ?? 1, outlineColor = outlineColor ?? Colors.black, - outlineBlurSigma = outlineBlurSigma ?? 0, - super(key: key); + outlineBlurSigma = outlineBlurSigma ?? 0; @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/basic/popup_menu_button.dart b/lib/widgets/common/basic/popup_menu_button.dart index a6b224f78..36d18607f 100644 --- a/lib/widgets/common/basic/popup_menu_button.dart +++ b/lib/widgets/common/basic/popup_menu_button.dart @@ -4,41 +4,24 @@ class AvesPopupMenuButton extends PopupMenuButton { final VoidCallback? onMenuOpened; const AvesPopupMenuButton({ - Key? key, - required PopupMenuItemBuilder itemBuilder, - T? initialValue, - PopupMenuItemSelected? onSelected, - PopupMenuCanceled? onCanceled, - String? tooltip, - double? elevation, - EdgeInsetsGeometry padding = const EdgeInsets.all(8.0), - Widget? child, - Widget? icon, - Offset offset = Offset.zero, - bool enabled = true, - ShapeBorder? shape, - Color? color, - bool? enableFeedback, - double? iconSize, + super.key, + required super.itemBuilder, + super.initialValue, + super.onSelected, + super.onCanceled, + super.tooltip, + super.elevation, + super.padding = const EdgeInsets.all(8.0), + super.child, + super.icon, + super.offset = Offset.zero, + super.enabled = true, + super.shape, + super.color, + super.enableFeedback, + super.iconSize, this.onMenuOpened, - }) : super( - key: key, - itemBuilder: itemBuilder, - initialValue: initialValue, - onSelected: onSelected, - onCanceled: onCanceled, - tooltip: tooltip, - elevation: elevation, - padding: padding, - child: child, - icon: icon, - iconSize: iconSize, - offset: offset, - enabled: enabled, - shape: shape, - color: color, - enableFeedback: enableFeedback, - ); + }); @override PopupMenuButtonState createState() => _AvesPopupMenuButtonState(); diff --git a/lib/widgets/common/basic/query_bar.dart b/lib/widgets/common/basic/query_bar.dart index 909b5fe05..a19d0e813 100644 --- a/lib/widgets/common/basic/query_bar.dart +++ b/lib/widgets/common/basic/query_bar.dart @@ -12,13 +12,13 @@ class QueryBar extends StatefulWidget { final bool editable; const QueryBar({ - Key? key, + super.key, required this.queryNotifier, this.focusNode, this.icon, this.hintText, this.editable = true, - }) : super(key: key); + }); @override State createState() => _QueryBarState(); @@ -49,45 +49,48 @@ class _QueryBarState extends State { tooltip: context.l10n.clearTooltip, ); - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: TextField( - controller: _controller, - focusNode: widget.focusNode ?? FocusNode(), - decoration: InputDecoration( - icon: Padding( - padding: const EdgeInsetsDirectional.only(start: 16), - child: Icon(widget.icon ?? AIcons.filter), - ), - hintText: widget.hintText ?? MaterialLocalizations.of(context).searchFieldLabel, - hintStyle: Theme.of(context).inputDecorationTheme.hintStyle, - ), - textInputAction: TextInputAction.search, - onChanged: (s) => _debouncer(() => queryNotifier.value = s.trim()), - enabled: widget.editable, - ), - ), - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 16), - child: ValueListenableBuilder( - valueListenable: _controller, - builder: (context, value, child) => AnimatedSwitcher( - duration: Durations.appBarActionChangeAnimation, - transitionBuilder: (child, animation) => FadeTransition( - opacity: animation, - child: SizeTransition( - axis: Axis.horizontal, - sizeFactor: animation, - child: child, + return DefaultTextStyle( + style: Theme.of(context).textTheme.bodyText2!, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: _controller, + focusNode: widget.focusNode ?? FocusNode(), + decoration: InputDecoration( + icon: Padding( + padding: const EdgeInsetsDirectional.only(start: 16), + child: Icon(widget.icon ?? AIcons.filter), ), + hintText: widget.hintText ?? MaterialLocalizations.of(context).searchFieldLabel, + hintStyle: Theme.of(context).inputDecorationTheme.hintStyle, ), - child: value.text.isNotEmpty ? clearButton : const SizedBox(), + textInputAction: TextInputAction.search, + onChanged: (s) => _debouncer(() => queryNotifier.value = s.trim()), + enabled: widget.editable, ), ), - ) - ], + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 16), + child: ValueListenableBuilder( + valueListenable: _controller, + builder: (context, value, child) => AnimatedSwitcher( + duration: Durations.appBarActionChangeAnimation, + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: SizeTransition( + axis: Axis.horizontal, + sizeFactor: animation, + child: child, + ), + ), + child: value.text.isNotEmpty ? clearButton : const SizedBox(), + ), + ), + ), + ], + ), ); } } diff --git a/lib/widgets/common/basic/reselectable_radio_list_tile.dart b/lib/widgets/common/basic/reselectable_radio_list_tile.dart index 501c52118..5d88833c3 100644 --- a/lib/widgets/common/basic/reselectable_radio_list_tile.dart +++ b/lib/widgets/common/basic/reselectable_radio_list_tile.dart @@ -20,7 +20,7 @@ class ReselectableRadioListTile extends StatelessWidget { bool get checked => value == groupValue; const ReselectableRadioListTile({ - Key? key, + super.key, required this.value, required this.groupValue, required this.onChanged, @@ -35,8 +35,7 @@ class ReselectableRadioListTile extends StatelessWidget { this.selected = false, this.controlAffinity = ListTileControlAffinity.platform, this.autofocus = false, - }) : assert(!isThreeLine || subtitle != null), - super(key: key); + }) : assert(!isThreeLine || subtitle != null); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/basic/slider_list_tile.dart b/lib/widgets/common/basic/slider_list_tile.dart index 8dc65db1c..8997b687a 100644 --- a/lib/widgets/common/basic/slider_list_tile.dart +++ b/lib/widgets/common/basic/slider_list_tile.dart @@ -9,14 +9,14 @@ class SliderListTile extends StatelessWidget { final int? divisions; const SliderListTile({ - Key? key, + super.key, required this.title, required this.value, required this.onChanged, this.min = 0.0, this.max = 1.0, this.divisions, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/basic/wheel.dart b/lib/widgets/common/basic/wheel.dart index f1e9cbf34..1f47a87af 100644 --- a/lib/widgets/common/basic/wheel.dart +++ b/lib/widgets/common/basic/wheel.dart @@ -7,12 +7,12 @@ class WheelSelector extends StatefulWidget { final TextAlign textAlign; const WheelSelector({ - Key? key, + super.key, required this.valueNotifier, required this.values, required this.textStyle, required this.textAlign, - }) : super(key: key); + }); @override State> createState() => _WheelSelectorState(); @@ -41,39 +41,51 @@ class _WheelSelectorState extends State> { const background = Colors.transparent; final foreground = DefaultTextStyle.of(context).style.color!; - return Padding( - padding: const EdgeInsets.all(8), - child: SizedBox( - width: itemSize.width, - height: itemSize.height * 3, - child: ShaderMask( - shaderCallback: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - background, - foreground, - foreground, - background, - ], - ).createShader, - child: ListWheelScrollView( - controller: _controller, - physics: const FixedExtentScrollPhysics(parent: BouncingScrollPhysics()), - diameterRatio: 1.2, - itemExtent: itemSize.height, - squeeze: 1.3, - onSelectedItemChanged: (i) => valueNotifier.value = values[i], - children: values - .map((i) => SizedBox.fromSize( - size: itemSize, - child: Text( - '$i', - textAlign: widget.textAlign, - style: widget.textStyle, - ), - )) - .toList(), + return NotificationListener( + // cancel notification bubbling so that the dialog scroll bar + // does not misinterpret wheel scrolling for dialog content scrolling + onNotification: (notification) => true, + child: Padding( + padding: const EdgeInsets.all(8), + child: SizedBox( + width: itemSize.width, + height: itemSize.height * 3, + child: ShaderMask( + shaderCallback: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + background, + foreground, + foreground, + background, + ], + ).createShader, + child: Theme( + data: Theme.of(context).copyWith( + scrollbarTheme: ScrollbarThemeData( + thumbVisibility: MaterialStateProperty.all(false), + ), + ), + child: ListWheelScrollView( + controller: _controller, + physics: const FixedExtentScrollPhysics(parent: BouncingScrollPhysics()), + diameterRatio: 1.2, + itemExtent: itemSize.height, + squeeze: 1.3, + onSelectedItemChanged: (i) => valueNotifier.value = values[i], + children: values + .map((i) => SizedBox.fromSize( + size: itemSize, + child: Text( + '$i', + textAlign: widget.textAlign, + style: widget.textStyle, + ), + )) + .toList(), + ), + ), ), ), ), diff --git a/lib/widgets/common/behaviour/double_back_pop.dart b/lib/widgets/common/behaviour/double_back_pop.dart index d1bbfc8c3..258e43c04 100644 --- a/lib/widgets/common/behaviour/double_back_pop.dart +++ b/lib/widgets/common/behaviour/double_back_pop.dart @@ -12,9 +12,9 @@ class DoubleBackPopScope extends StatefulWidget { final Widget child; const DoubleBackPopScope({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); @override State createState() => _DoubleBackPopScopeState(); diff --git a/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart b/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart index 0d89ff6a2..9e8ffb093 100644 --- a/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart +++ b/lib/widgets/common/behaviour/eager_scale_gesture_recognizer.dart @@ -1,3 +1,4 @@ +// ignore_for_file: depend_on_referenced_packages import 'dart:math' as math; import 'package:flutter/gestures.dart'; diff --git a/lib/widgets/common/behaviour/routes.dart b/lib/widgets/common/behaviour/routes.dart index 4e90b00cb..a9f5760f9 100644 --- a/lib/widgets/common/behaviour/routes.dart +++ b/lib/widgets/common/behaviour/routes.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; class DirectPageTransitionsTheme extends PageTransitionsTheme { + const DirectPageTransitionsTheme(); + @override Widget buildTransitions( PageRoute route, diff --git a/lib/widgets/common/behaviour/sloppy_scroll_physics.dart b/lib/widgets/common/behaviour/sloppy_scroll_physics.dart index 072c57d4d..67b321183 100644 --- a/lib/widgets/common/behaviour/sloppy_scroll_physics.dart +++ b/lib/widgets/common/behaviour/sloppy_scroll_physics.dart @@ -1,25 +1,30 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; +// TODO TLAD merge with `MagnifierScrollerPhysics` class SloppyScrollPhysics extends ScrollPhysics { + final DeviceGestureSettings? gestureSettings; + + // in [0, 1] + // 0: most reactive but will not let Magnifier recognizers accept gestures + // 1: less reactive but gives the most leeway to Magnifier recognizers + final double touchSlopFactor; + const SloppyScrollPhysics({ + required this.gestureSettings, this.touchSlopFactor = 1, ScrollPhysics? parent, }) : super(parent: parent); - // in [0, 1] - // 0: most reactive but will not let other recognizers accept gestures - // 1: less reactive but gives the most leeway to other recognizers - final double touchSlopFactor; - @override SloppyScrollPhysics applyTo(ScrollPhysics? ancestor) { return SloppyScrollPhysics( + gestureSettings: gestureSettings, touchSlopFactor: touchSlopFactor, parent: buildParent(ancestor), ); } @override - double get dragStartDistanceMotionThreshold => kTouchSlop * touchSlopFactor; + double get dragStartDistanceMotionThreshold => (gestureSettings?.touchSlop ?? kTouchSlop) * touchSlopFactor; } diff --git a/lib/widgets/common/expandable_filter_row.dart b/lib/widgets/common/expandable_filter_row.dart index 0356a6461..fc3083d93 100644 --- a/lib/widgets/common/expandable_filter_row.dart +++ b/lib/widgets/common/expandable_filter_row.dart @@ -15,7 +15,7 @@ class ExpandableFilterRow extends StatelessWidget { final OffsetFilterCallback? onLongPress; const ExpandableFilterRow({ - Key? key, + super.key, this.title, required this.filters, required this.expandedNotifier, @@ -23,7 +23,7 @@ class ExpandableFilterRow extends StatelessWidget { this.heroTypeBuilder, required this.onTap, required this.onLongPress, - }) : super(key: key); + }); static const double horizontalPadding = 8; static const double verticalPadding = 8; @@ -78,7 +78,6 @@ class ExpandableFilterRow extends StatelessWidget { height: AvesFilterChip.minChipHeight, child: ListView.separated( scrollDirection: Axis.horizontal, - physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: horizontalPadding), itemBuilder: (context, index) { return index < filterList.length ? _buildFilterChip(filterList[index]) : const SizedBox(); diff --git a/lib/widgets/common/favourite_toggler.dart b/lib/widgets/common/favourite_toggler.dart index 340e9d0ee..0b3e858af 100644 --- a/lib/widgets/common/favourite_toggler.dart +++ b/lib/widgets/common/favourite_toggler.dart @@ -14,11 +14,11 @@ class FavouriteToggler extends StatefulWidget { final VoidCallback? onPressed; const FavouriteToggler({ - Key? key, + super.key, required this.entries, this.isMenuItem = false, this.onPressed, - }) : super(key: key); + }); @override State createState() => _FavouriteTogglerState(); diff --git a/lib/widgets/common/fx/blurred.dart b/lib/widgets/common/fx/blurred.dart index 91131ac39..c3374401a 100644 --- a/lib/widgets/common/fx/blurred.dart +++ b/lib/widgets/common/fx/blurred.dart @@ -9,10 +9,10 @@ class BlurredRect extends StatelessWidget { final Widget child; const BlurredRect({ - Key? key, + super.key, this.enabled = true, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -33,11 +33,11 @@ class BlurredRRect extends StatelessWidget { final Widget child; const BlurredRRect({ - Key? key, + super.key, this.enabled = true, required this.borderRadius, required this.child, - }) : super(key: key); + }); factory BlurredRRect.all({ Key? key, @@ -72,10 +72,10 @@ class BlurredOval extends StatelessWidget { final Widget child; const BlurredOval({ - Key? key, + super.key, this.enabled = true, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/fx/highlight_decoration.dart b/lib/widgets/common/fx/highlight_decoration.dart index 669fdd94d..8deec871b 100644 --- a/lib/widgets/common/fx/highlight_decoration.dart +++ b/lib/widgets/common/fx/highlight_decoration.dart @@ -6,7 +6,7 @@ class HighlightDecoration extends Decoration { const HighlightDecoration({required this.color}); @override - _HighlightDecorationPainter createBoxPainter([VoidCallback? onChanged]) { + BoxPainter createBoxPainter([VoidCallback? onChanged]) { return _HighlightDecorationPainter(this, onChanged); } } diff --git a/lib/widgets/common/fx/sweeper.dart b/lib/widgets/common/fx/sweeper.dart index 6062016cb..a02add989 100644 --- a/lib/widgets/common/fx/sweeper.dart +++ b/lib/widgets/common/fx/sweeper.dart @@ -15,7 +15,7 @@ class Sweeper extends StatefulWidget { final VoidCallback? onSweepEnd; const Sweeper({ - Key? key, + super.key, required this.builder, this.startAngle = -pi / 2, this.sweepAngle = pi / 4, @@ -23,7 +23,7 @@ class Sweeper extends StatefulWidget { required this.toggledNotifier, this.centerSweep = true, this.onSweepEnd, - }) : super(key: key); + }); @override State createState() => _SweeperState(); diff --git a/lib/widgets/common/fx/transition_image.dart b/lib/widgets/common/fx/transition_image.dart index 33d5b2472..cad92fddb 100644 --- a/lib/widgets/common/fx/transition_image.dart +++ b/lib/widgets/common/fx/transition_image.dart @@ -16,13 +16,13 @@ class TransitionImage extends StatefulWidget { final Color? background; const TransitionImage({ - Key? key, + super.key, required this.image, required this.animation, this.width, this.height, this.background, - }) : super(key: key); + }); @override State createState() => _TransitionImageState(); diff --git a/lib/widgets/common/grid/draggable_thumb_label.dart b/lib/widgets/common/grid/draggable_thumb_label.dart index 65ae604e1..af6888457 100644 --- a/lib/widgets/common/grid/draggable_thumb_label.dart +++ b/lib/widgets/common/grid/draggable_thumb_label.dart @@ -5,15 +5,35 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +class DraggableCrumbLabel extends StatelessWidget { + final String label; + + const DraggableCrumbLabel({ + super.key, + required this.label, + }); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: _crumbLabelMaxWidth), + child: Padding( + padding: _padding, + child: _buildText(label, isCrumb: true), + ), + ); + } +} + class DraggableThumbLabel extends StatelessWidget { final double offsetY; final List Function(BuildContext context, T item) lineBuilder; const DraggableThumbLabel({ - Key? key, + super.key, required this.offsetY, required this.lineBuilder, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -30,30 +50,20 @@ class DraggableThumbLabel extends StatelessWidget { if (lines.isEmpty) return const SizedBox(); return ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 140), + constraints: const BoxConstraints(maxWidth: _thumbLabelMaxWidth), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + padding: _padding, child: lines.length > 1 ? Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, - children: lines.map(_buildText).toList(), + children: lines.map((v) => _buildText(v, isCrumb: false)).toList(), ) - : _buildText(lines.first), + : _buildText(lines.first, isCrumb: false), ), ); } - Widget _buildText(String text) => Text( - text, - style: const TextStyle( - color: Colors.black, - ), - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - ); - static String formatMonthThumbLabel(BuildContext context, DateTime? date) { final l10n = context.l10n; if (date == null) return l10n.sectionUnknown; @@ -66,3 +76,18 @@ class DraggableThumbLabel extends StatelessWidget { return formatDay(date, l10n.localeName); } } + +const double _crumbLabelMaxWidth = 96; +const double _thumbLabelMaxWidth = 144; +const EdgeInsets _padding = EdgeInsets.symmetric(vertical: 4, horizontal: 8); + +Widget _buildText(String text, {required bool isCrumb}) => Text( + text, + style: TextStyle( + color: Colors.black, + fontSize: isCrumb ? 10 : 14, + ), + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ); diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart index bcb0e40e0..d5a4bbf2f 100644 --- a/lib/widgets/common/grid/header.dart +++ b/lib/widgets/common/grid/header.dart @@ -16,15 +16,15 @@ class SectionHeader extends StatelessWidget { final bool selectable; const SectionHeader({ - Key? key, + super.key, required this.sectionKey, this.leading, required this.title, this.trailing, this.selectable = true, - }) : super(key: key); + }); - static const leadingDimension = 32.0; + static const leadingSize = Size(48, 32); static const padding = EdgeInsets.all(16); static const widgetSpanAlignment = PlaceholderAlignment.middle; @@ -33,7 +33,7 @@ class SectionHeader extends StatelessWidget { return Container( alignment: AlignmentDirectional.centerStart, padding: padding, - constraints: const BoxConstraints(minHeight: leadingDimension), + constraints: BoxConstraints(minHeight: leadingSize.height), child: GestureDetector( onTap: selectable ? () => _toggleSectionSelection(context) : null, child: Text.rich( @@ -47,14 +47,16 @@ class SectionHeader extends StatelessWidget { browsingBuilder: leading != null ? (context) => Container( padding: const EdgeInsetsDirectional.only(end: 8, bottom: 4), - width: leadingDimension, - height: leadingDimension, + width: leadingSize.width, + height: leadingSize.height, child: leading, ) : null, onPressed: selectable ? () => _toggleSectionSelection(context) : null, ), ), + // TODO TLAD [flutter 3] remove this zero-width span when this is fixed: https://github.com/flutter/flutter/issues/103615 + TextSpan(text: Constants.zwsp * 3, style: Constants.titleTextStyle), TextSpan( text: title, style: Constants.titleTextStyle, @@ -127,14 +129,12 @@ class _SectionSelectableLeading extends StatelessWidget { final VoidCallback? onPressed; const _SectionSelectableLeading({ - Key? key, + super.key, this.selectable = true, required this.sectionKey, required this.browsingBuilder, required this.onPressed, - }) : super(key: key); - - static const leadingDimension = SectionHeader.leadingDimension; + }); @override Widget build(BuildContext context) { @@ -172,7 +172,7 @@ class _SectionSelectableLeading extends StatelessWidget { ); } - Widget _buildBrowsing(BuildContext context) => browsingBuilder?.call(context) ?? const SizedBox(height: leadingDimension); + Widget _buildBrowsing(BuildContext context) => browsingBuilder?.call(context) ?? SizedBox(height: SectionHeader.leadingSize.height); } class _SectionSelectingLeading extends StatelessWidget { @@ -180,10 +180,10 @@ class _SectionSelectingLeading extends StatelessWidget { final VoidCallback? onPressed; const _SectionSelectingLeading({ - Key? key, + super.key, required this.sectionKey, required this.onPressed, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -205,15 +205,14 @@ class _SectionSelectingLeading extends StatelessWidget { ), child: IconButton( iconSize: 26, - padding: const EdgeInsets.only(top: 1), - alignment: AlignmentDirectional.topStart, - icon: Icon(isSelected ? AIcons.selected : AIcons.unselected), + padding: const EdgeInsetsDirectional.only(end: 6, bottom: 4), onPressed: onPressed, tooltip: isSelected ? context.l10n.collectionDeselectSectionTooltip : context.l10n.collectionSelectSectionTooltip, - constraints: const BoxConstraints( - minHeight: SectionHeader.leadingDimension, - minWidth: SectionHeader.leadingDimension, + constraints: BoxConstraints( + minHeight: SectionHeader.leadingSize.height, + minWidth: SectionHeader.leadingSize.width, ), + icon: Icon(isSelected ? AIcons.selected : AIcons.unselected), ), ), ); diff --git a/lib/widgets/common/grid/item_tracker.dart b/lib/widgets/common/grid/item_tracker.dart index bceaf8dd8..8f5755999 100644 --- a/lib/widgets/common/grid/item_tracker.dart +++ b/lib/widgets/common/grid/item_tracker.dart @@ -17,13 +17,13 @@ class GridItemTracker extends StatefulWidget { final Widget child; const GridItemTracker({ - Key? key, + super.key, required this.scrollableKey, required this.appBarHeightNotifier, required this.tileLayout, required this.scrollController, required this.child, - }) : super(key: key); + }); @override State> createState() => _GridItemTrackerState(); @@ -45,7 +45,7 @@ class _GridItemTrackerState extends State> with WidgetsBin } Orientation get _windowOrientation { - final size = WidgetsBinding.instance!.window.physicalSize; + final size = WidgetsBinding.instance.window.physicalSize; return size.width > size.height ? Orientation.landscape : Orientation.portrait; } @@ -62,7 +62,7 @@ class _GridItemTrackerState extends State> with WidgetsBin final highlightInfo = context.read(); _subscriptions.add(highlightInfo.eventBus.on>().listen(_trackItem)); _lastOrientation = _windowOrientation; - WidgetsBinding.instance!.addObserver(this); + WidgetsBinding.instance.addObserver(this); _saveLayoutMetrics(); } @@ -90,7 +90,7 @@ class _GridItemTrackerState extends State> with WidgetsBin @override void dispose() { - WidgetsBinding.instance!.removeObserver(this); + WidgetsBinding.instance.removeObserver(this); _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); @@ -168,7 +168,7 @@ class _GridItemTrackerState extends State> with WidgetsBin } if (pivotItem != null) { - WidgetsBinding.instance!.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { context.read().trackItem(pivotItem, animate: false); }); } diff --git a/lib/widgets/common/grid/overlay.dart b/lib/widgets/common/grid/overlay.dart index 4af0f83d2..a7fc392de 100644 --- a/lib/widgets/common/grid/overlay.dart +++ b/lib/widgets/common/grid/overlay.dart @@ -13,11 +13,11 @@ class GridItemSelectionOverlay extends StatelessWidget { static const duration = Durations.thumbnailOverlayAnimation; const GridItemSelectionOverlay({ - Key? key, + super.key, required this.item, this.borderRadius, this.padding, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/grid/scaling.dart b/lib/widgets/common/grid/scaling.dart index 279f3fa27..e24646e04 100644 --- a/lib/widgets/common/grid/scaling.dart +++ b/lib/widgets/common/grid/scaling.dart @@ -31,7 +31,7 @@ class GridScaleGestureDetector extends StatefulWidget { final Widget child; const GridScaleGestureDetector({ - Key? key, + super.key, required this.scrollableKey, required this.tileLayout, required this.heightForWidth, @@ -39,7 +39,7 @@ class GridScaleGestureDetector extends StatefulWidget { required this.scaledBuilder, this.highlightItem, required this.child, - }) : super(key: key); + }); @override State> createState() => _GridScaleGestureDetectorState(); @@ -55,6 +55,8 @@ class _GridScaleGestureDetectorState extends State((mq) => mq.gestureSettings); + final child = GestureDetector( // Horizontal/vertical drag gestures are interpreted as scaling // if they are not handled by `onHorizontalDragStart`/`onVerticalDragStart` @@ -79,7 +81,8 @@ class _GridScaleGestureDetectorState extends State extends State().trackItem(trackItem, animate: false, highlightItem: highlightItem); @@ -233,7 +236,6 @@ class _ScaleOverlay extends StatefulWidget { final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder; const _ScaleOverlay({ - Key? key, required this.builder, required this.tileLayout, required this.center, @@ -241,7 +243,7 @@ class _ScaleOverlay extends StatefulWidget { required this.xMax, required this.scaledSizeNotifier, required this.gridBuilder, - }) : super(key: key); + }); @override State<_ScaleOverlay> createState() => _ScaleOverlayState(); @@ -263,7 +265,7 @@ class _ScaleOverlayState extends State<_ScaleOverlay> { @override void initState() { super.initState(); - WidgetsBinding.instance!.addPostFrameCallback((_) => setState(() => _init = true)); + WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => _init = true)); } @override diff --git a/lib/widgets/common/grid/section_layout.dart b/lib/widgets/common/grid/section_layout.dart index 42f2dd021..cb6ef28b5 100644 --- a/lib/widgets/common/grid/section_layout.dart +++ b/lib/widgets/common/grid/section_layout.dart @@ -21,7 +21,7 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { final Widget child; const SectionedListLayoutProvider({ - Key? key, + super.key, required this.scrollableWidth, required this.tileLayout, required int columnCount, @@ -34,8 +34,7 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { required this.child, }) : assert(scrollableWidth != 0), columnCount = tileLayout == TileLayout.list ? 1 : columnCount, - tileWidth = tileLayout == TileLayout.list ? scrollableWidth - (horizontalPadding * 2) : tileWidth, - super(key: key); + tileWidth = tileLayout == TileLayout.list ? scrollableWidth - (horizontalPadding * 2) : tileWidth; @override Widget build(BuildContext context) { @@ -296,13 +295,12 @@ class _GridRow extends MultiChildRenderObjectWidget { final TextDirection textDirection; _GridRow({ - Key? key, required this.width, required this.height, required this.spacing, required this.textDirection, required List children, - }) : super(key: key, children: children); + }) : super(children: children); @override RenderObject createRenderObject(BuildContext context) { diff --git a/lib/widgets/common/grid/selector.dart b/lib/widgets/common/grid/selector.dart index 907166958..4a53f5607 100644 --- a/lib/widgets/common/grid/selector.dart +++ b/lib/widgets/common/grid/selector.dart @@ -18,14 +18,14 @@ class GridSelectionGestureDetector extends StatefulWidget { final Widget child; const GridSelectionGestureDetector({ - Key? key, + super.key, required this.scrollableKey, this.selectable = true, required this.items, required this.scrollController, required this.appBarHeightNotifier, required this.child, - }) : super(key: key); + }); @override State> createState() => _GridSelectionGestureDetectorState(); diff --git a/lib/widgets/common/grid/sliver.dart b/lib/widgets/common/grid/sliver.dart index d322a5a4f..dffeeb407 100644 --- a/lib/widgets/common/grid/sliver.dart +++ b/lib/widgets/common/grid/sliver.dart @@ -14,7 +14,7 @@ import 'package:provider/provider.dart'; // cf https://github.com/flutter/flutter/issues/49027 // adapted from Flutter `RenderSliverFixedExtentBoxAdaptor` in `/rendering/sliver_fixed_extent_list.dart` class SectionedListSliver extends StatelessWidget { - const SectionedListSliver({Key? key}) : super(key: key); + const SectionedListSliver({super.key}); @override Widget build(BuildContext context) { @@ -40,10 +40,9 @@ class _SliverKnownExtentList extends SliverMultiBoxAdaptorWidget { final List sectionLayouts; const _SliverKnownExtentList({ - Key? key, required SliverChildDelegate delegate, required this.sectionLayouts, - }) : super(key: key, delegate: delegate); + }) : super(delegate: delegate); @override _RenderSliverKnownExtentBoxAdaptor createRenderObject(BuildContext context) { @@ -79,7 +78,7 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { SectionLayout? sectionAtOffset(double scrollOffset) => sectionLayouts.firstWhereOrNull((section) => section.hasChildAtOffset(scrollOffset)) ?? sectionLayouts.lastOrNull; double indexToLayoutOffset(int index) { - return (sectionAtIndex(index) ?? sectionLayouts.last).indexToLayoutOffset(index); + return (sectionAtIndex(index) ?? sectionLayouts.lastOrNull)?.indexToLayoutOffset(index) ?? 0; } int getMinChildIndexForScrollOffset(double scrollOffset) { diff --git a/lib/widgets/common/grid/theme.dart b/lib/widgets/common/grid/theme.dart index 670c9995e..4ab591721 100644 --- a/lib/widgets/common/grid/theme.dart +++ b/lib/widgets/common/grid/theme.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -10,18 +11,19 @@ class GridTheme extends StatelessWidget { final Widget child; const GridTheme({ - Key? key, + super.key, required this.extent, this.showLocation, this.showTrash, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { return ProxyProvider2( update: (context, settings, mq, previous) { - var iconSize = min(24.0, (extent / 5)).roundToDouble(); + final margin = OverlayIcon.defaultMargin.vertical; + var iconSize = min(24.0, ((extent - margin) / 5).floorToDouble() - margin); final fontSize = (iconSize * .7).floorToDouble(); iconSize *= mq.textScaleFactor; final highlightBorderWidth = extent * .1; @@ -34,6 +36,7 @@ class GridTheme extends StatelessWidget { showMotionPhoto: settings.showThumbnailMotionPhoto, showRating: settings.showThumbnailRating, showRaw: settings.showThumbnailRaw, + showTag: settings.showThumbnailTag, showTrash: showTrash ?? true, showVideoDuration: settings.showThumbnailVideoDuration, ); @@ -45,7 +48,7 @@ class GridTheme extends StatelessWidget { class GridThemeData { final double iconSize, fontSize, highlightBorderWidth; - final bool showFavourite, showLocation, showMotionPhoto, showRating, showRaw, showTrash, showVideoDuration; + final bool showFavourite, showLocation, showMotionPhoto, showRating, showRaw, showTag, showTrash, showVideoDuration; const GridThemeData({ required this.iconSize, @@ -56,6 +59,7 @@ class GridThemeData { required this.showMotionPhoto, required this.showRating, required this.showRaw, + required this.showTag, required this.showTrash, required this.showVideoDuration, }); diff --git a/lib/widgets/common/identity/aves_app_bar.dart b/lib/widgets/common/identity/aves_app_bar.dart new file mode 100644 index 000000000..8dcadd5f0 --- /dev/null +++ b/lib/widgets/common/identity/aves_app_bar.dart @@ -0,0 +1,231 @@ +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/aves_app.dart'; +import 'package:aves/widgets/common/fx/blurred.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AvesAppBar extends StatelessWidget { + final double contentHeight; + final Widget leading; + final Widget title; + final List actions; + final Widget? bottom; + final Object? transitionKey; + + static const leadingHeroTag = 'appbar-leading'; + static const titleHeroTag = 'appbar-title'; + + const AvesAppBar({ + super.key, + required this.contentHeight, + required this.leading, + required this.title, + required this.actions, + this.bottom, + this.transitionKey, + }); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (context, mq) => mq.padding.top, + builder: (context, mqPaddingTop, child) { + return SliverPersistentHeader( + floating: true, + pinned: false, + delegate: _SliverAppBarDelegate( + height: mqPaddingTop + appBarHeightForContentHeight(contentHeight), + child: SafeArea( + bottom: false, + child: AvesFloatingBar( + builder: (context, backgroundColor, child) => Material( + color: backgroundColor, + textStyle: Theme.of(context).appBarTheme.titleTextStyle, + child: child, + ), + child: Column( + children: [ + SizedBox( + height: kToolbarHeight, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Hero( + tag: leadingHeroTag, + flightShuttleBuilder: _flightShuttleBuilder, + transitionOnUserGestures: true, + child: leading, + ), + ), + Expanded( + child: Hero( + tag: titleHeroTag, + flightShuttleBuilder: _flightShuttleBuilder, + transitionOnUserGestures: true, + child: AnimatedSwitcher( + duration: context.read().iconAnimation, + child: Row( + key: ValueKey(transitionKey), + children: [ + Expanded(child: title), + ...actions, + ], + ), + ), + ), + ), + ], + ), + ), + if (bottom != null) bottom!, + ], + ), + ), + ), + ), + ); + }, + ); + } + + static Widget _flightShuttleBuilder( + BuildContext flightContext, + Animation animation, + HeroFlightDirection direction, + BuildContext fromHero, + BuildContext toHero, + ) { + final pushing = direction == HeroFlightDirection.push; + Widget popBuilder(context, child) => Opacity(opacity: 1 - animation.value, child: child); + Widget pushBuilder(context, child) => Opacity(opacity: animation.value, child: child); + return Material( + type: MaterialType.transparency, + child: DefaultTextStyle( + style: DefaultTextStyle.of(toHero).style, + child: Stack( + children: [ + AnimatedBuilder( + animation: animation, + builder: pushing ? popBuilder : pushBuilder, + child: fromHero.widget, + ), + AnimatedBuilder( + animation: animation, + builder: pushing ? pushBuilder : popBuilder, + child: toHero.widget, + ), + ], + ), + ), + ); + } + + static double appBarHeightForContentHeight(double contentHeight) => AvesFloatingBar.margin.vertical + contentHeight; +} + +class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { + final double height; + final Widget child; + + const _SliverAppBarDelegate({ + required this.height, + required this.child, + }); + + @override + double get minExtent => height; + + @override + double get maxExtent => height; + + @override + Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => child; + + @override + bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) => true; +} + +class AvesFloatingBar extends StatefulWidget { + final Widget Function(BuildContext context, Color backgroundColor, Widget? child) builder; + final Widget? child; + + static const margin = EdgeInsets.all(8); + static const borderRadius = BorderRadius.all(Radius.circular(8)); + + const AvesFloatingBar({ + super.key, + required this.builder, + this.child, + }); + + @override + State createState() => _AvesFloatingBarState(); +} + +class _AvesFloatingBarState extends State with RouteAware { + // prevent expensive blurring when the current page is hidden + final ValueNotifier _isBlurAllowedNotifier = ValueNotifier(true); + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final route = ModalRoute.of(context); + if (route is PageRoute) { + AvesApp.pageRouteObserver.subscribe(this, route); + } + } + + @override + void dispose() { + AvesApp.pageRouteObserver.unsubscribe(this); + super.dispose(); + } + + @override + void didPopNext() { + // post to prevent single frame flash during hero + WidgetsBinding.instance.addPostFrameCallback((_) { + _isBlurAllowedNotifier.value = true; + }); + } + + @override + void didPushNext() { + // post to prevent single frame flash during hero + WidgetsBinding.instance.addPostFrameCallback((_) { + _isBlurAllowedNotifier.value = false; + }); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final backgroundColor = theme.appBarTheme.backgroundColor!; + return ValueListenableBuilder( + valueListenable: _isBlurAllowedNotifier, + builder: (context, isBlurAllowed, child) { + final blurred = isBlurAllowed && context.select((s) => s.enableOverlayBlurEffect); + return Container( + foregroundDecoration: BoxDecoration( + border: Border.all( + color: theme.dividerColor, + ), + borderRadius: AvesFloatingBar.borderRadius, + ), + margin: AvesFloatingBar.margin, + child: BlurredRRect( + enabled: blurred, + borderRadius: AvesFloatingBar.borderRadius, + child: widget.builder( + context, + blurred ? backgroundColor.withOpacity(.85) : backgroundColor, + widget.child, + ), + ), + ); + }, + ); + } +} diff --git a/lib/widgets/common/identity/aves_expansion_tile.dart b/lib/widgets/common/identity/aves_expansion_tile.dart index 34c4e019d..c929a1b73 100644 --- a/lib/widgets/common/identity/aves_expansion_tile.dart +++ b/lib/widgets/common/identity/aves_expansion_tile.dart @@ -14,7 +14,7 @@ class AvesExpansionTile extends StatelessWidget { final List children; const AvesExpansionTile({ - Key? key, + super.key, String? value, this.leading, required this.title, @@ -23,8 +23,7 @@ class AvesExpansionTile extends StatelessWidget { this.initiallyExpanded = false, this.showHighlight = true, required this.children, - }) : value = value ?? title, - super(key: key); + }) : value = value ?? title; @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index dbcbc09cc..21dfe3297 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -60,7 +60,7 @@ class AvesFilterChip extends StatefulWidget { static const double decoratedContentVerticalPadding = 5; const AvesFilterChip({ - Key? key, + super.key, required this.filter, this.removable = false, this.showText = true, @@ -75,7 +75,7 @@ class AvesFilterChip extends StatefulWidget { this.heroType = HeroType.onTap, this.onTap, this.onLongPress = showDefaultLongPressMenu, - }) : super(key: key); + }); static Future showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async { if (context.read>().value == AppMode.main) { @@ -139,7 +139,7 @@ class _AvesFilterChipState extends State { _subscriptions.add(covers.colorChangeStream.listen(_onCoverColorChange)); _subscriptions.add(settings.updateStream.where((event) => event.key == Settings.themeColorModeKey).listen((_) { // delay so that contextual colors reflect the new settings - WidgetsBinding.instance!.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { _onCoverColorChange(null); }); })); @@ -282,7 +282,7 @@ class _AvesFilterChipState extends State { onTapDown: onLongPress != null ? (details) => _tapPosition = details.globalPosition : null, onTap: onTap != null ? () { - WidgetsBinding.instance!.addPostFrameCallback((_) => onTap!(filter)); + WidgetsBinding.instance.addPostFrameCallback((_) => onTap!(filter)); setState(() => _tapped = true); } : null, diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index 53448ec14..a32ec6d71 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -12,9 +12,9 @@ class VideoIcon extends StatelessWidget { final AvesEntry entry; const VideoIcon({ - Key? key, + super.key, required this.entry, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -38,7 +38,7 @@ class VideoIcon extends StatelessWidget { } class AnimatedImageIcon extends StatelessWidget { - const AnimatedImageIcon({Key? key}) : super(key: key); + const AnimatedImageIcon({super.key}); static const scale = .8; @@ -52,7 +52,7 @@ class AnimatedImageIcon extends StatelessWidget { } class GeoTiffIcon extends StatelessWidget { - const GeoTiffIcon({Key? key}) : super(key: key); + const GeoTiffIcon({super.key}); @override Widget build(BuildContext context) { @@ -63,7 +63,7 @@ class GeoTiffIcon extends StatelessWidget { } class SphericalImageIcon extends StatelessWidget { - const SphericalImageIcon({Key? key}) : super(key: key); + const SphericalImageIcon({super.key}); @override Widget build(BuildContext context) { @@ -74,7 +74,7 @@ class SphericalImageIcon extends StatelessWidget { } class FavouriteIcon extends StatelessWidget { - const FavouriteIcon({Key? key}) : super(key: key); + const FavouriteIcon({super.key}); static const scale = .9; @@ -87,8 +87,23 @@ class FavouriteIcon extends StatelessWidget { } } +class TagIcon extends StatelessWidget { + const TagIcon({super.key}); + + static const scale = .9; + + @override + Widget build(BuildContext context) { + return const OverlayIcon( + icon: AIcons.tag, + iconScale: scale, + relativeOffset: Offset(.05, .05), + ); + } +} + class GpsIcon extends StatelessWidget { - const GpsIcon({Key? key}) : super(key: key); + const GpsIcon({super.key}); @override Widget build(BuildContext context) { @@ -99,7 +114,7 @@ class GpsIcon extends StatelessWidget { } class RawIcon extends StatelessWidget { - const RawIcon({Key? key}) : super(key: key); + const RawIcon({super.key}); @override Widget build(BuildContext context) { @@ -110,7 +125,7 @@ class RawIcon extends StatelessWidget { } class MotionPhotoIcon extends StatelessWidget { - const MotionPhotoIcon({Key? key}) : super(key: key); + const MotionPhotoIcon({super.key}); static const scale = .8; @@ -129,9 +144,9 @@ class MultiPageIcon extends StatelessWidget { static const scale = .8; const MultiPageIcon({ - Key? key, + super.key, required this.entry, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -157,9 +172,9 @@ class RatingIcon extends StatelessWidget { final AvesEntry entry; const RatingIcon({ - Key? key, + super.key, required this.entry, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -180,9 +195,9 @@ class TrashIcon extends StatelessWidget { final int? trashDaysLeft; const TrashIcon({ - Key? key, + super.key, required this.trashDaysLeft, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -204,34 +219,48 @@ class OverlayIcon extends StatelessWidget { final IconData icon; final String? text; final double iconScale; - final EdgeInsets margin; + final EdgeInsetsGeometry margin; + final Offset? relativeOffset; + + static const defaultMargin = EdgeInsets.only(left: 1, right: 1, bottom: 1); const OverlayIcon({ - Key? key, + super.key, required this.icon, this.iconScale = 1, this.text, // default margin for multiple icons in a `Column` - this.margin = const EdgeInsets.only(left: 1, right: 1, bottom: 1), - }) : super(key: key); + this.margin = defaultMargin, + this.relativeOffset, + }); @override Widget build(BuildContext context) { final size = context.select((t) => t.iconSize); - final iconChild = Icon( + Widget iconChild = Icon( icon, size: size, ); - final iconBox = SizedBox( + + if (relativeOffset != null) { + iconChild = FractionalTranslation( + translation: relativeOffset!, + child: iconChild, + ); + } + + if (iconScale != 1) { + // using a transform is better than modifying the icon size to properly center the scaled icon + iconChild = Transform.scale( + scale: iconScale, + child: iconChild, + ); + } + + iconChild = SizedBox( width: size, height: size, - // using a transform is better than modifying the icon size to properly center the scaled icon - child: iconScale != 1 - ? Transform.scale( - scale: iconScale, - child: iconChild, - ) - : iconChild, + child: iconChild, ); return Container( @@ -242,12 +271,12 @@ class OverlayIcon extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(size)), ), child: text == null - ? iconBox + ? iconChild : Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - iconBox, + iconChild, const SizedBox(width: 2), Text( text!, diff --git a/lib/widgets/common/identity/aves_logo.dart b/lib/widgets/common/identity/aves_logo.dart index 23fb7a04e..19a4b161c 100644 --- a/lib/widgets/common/identity/aves_logo.dart +++ b/lib/widgets/common/identity/aves_logo.dart @@ -9,9 +9,9 @@ class AvesLogo extends StatelessWidget { final double size; const AvesLogo({ - Key? key, + super.key, required this.size, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/identity/buttons.dart b/lib/widgets/common/identity/buttons.dart index 3897ee2d6..9b3ecc2f8 100644 --- a/lib/widgets/common/identity/buttons.dart +++ b/lib/widgets/common/identity/buttons.dart @@ -6,11 +6,11 @@ class AvesOutlinedButton extends StatelessWidget { final VoidCallback? onPressed; const AvesOutlinedButton({ - Key? key, + super.key, this.icon, required this.label, required this.onPressed, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/identity/empty.dart b/lib/widgets/common/identity/empty.dart index af4ff6096..9c40675df 100644 --- a/lib/widgets/common/identity/empty.dart +++ b/lib/widgets/common/identity/empty.dart @@ -10,13 +10,13 @@ class EmptyContent extends StatelessWidget { final bool safeBottom; const EmptyContent({ - Key? key, + super.key, this.icon, required this.text, this.alignment = const FractionalOffset(.5, .35), this.fontSize = 22, this.safeBottom = true, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/identity/highlight_title.dart b/lib/widgets/common/identity/highlight_title.dart index a8976cb8d..64e263e96 100644 --- a/lib/widgets/common/identity/highlight_title.dart +++ b/lib/widgets/common/identity/highlight_title.dart @@ -15,14 +15,14 @@ class HighlightTitle extends StatelessWidget { final bool showHighlight; const HighlightTitle({ - Key? key, + super.key, required this.title, this.color, this.fontSize = 18, this.enabled = true, this.selectable = false, this.showHighlight = true, - }) : super(key: key); + }); static const disabledColor = Colors.grey; diff --git a/lib/widgets/common/identity/scroll_thumb.dart b/lib/widgets/common/identity/scroll_thumb.dart index 3b6ccf73f..4fdf08c40 100644 --- a/lib/widgets/common/identity/scroll_thumb.dart +++ b/lib/widgets/common/identity/scroll_thumb.dart @@ -15,12 +15,12 @@ ScrollThumbBuilder avesScrollThumbBuilder({ borderRadius: BorderRadius.all(Radius.circular(12)), ), height: height, - margin: const EdgeInsetsDirectional.only(end: 1), - padding: const EdgeInsets.all(2), + margin: _margin, + padding: _padding, child: ClipPath( clipper: ArrowClipper(), child: Container( - width: 20.0, + width: _width, decoration: BoxDecoration( color: backgroundColor, borderRadius: const BorderRadius.all(Radius.circular(12)), @@ -38,3 +38,9 @@ ScrollThumbBuilder avesScrollThumbBuilder({ ); }; } + +const _margin = EdgeInsetsDirectional.only(end: 1); +const _padding = EdgeInsets.all(2); +const _width = 20.0; + +double get avesScrollThumbWidth => _width + _padding.horizontal + _margin.horizontal; diff --git a/lib/widgets/common/magnifier/core/core.dart b/lib/widgets/common/magnifier/core/core.dart index 495a857f6..9364dcbc2 100644 --- a/lib/widgets/common/magnifier/core/core.dart +++ b/lib/widgets/common/magnifier/core/core.dart @@ -23,7 +23,7 @@ class MagnifierCore extends StatefulWidget { final Widget child; const MagnifierCore({ - Key? key, + super.key, required this.controller, required this.scaleStateCycle, required this.applyScale, @@ -31,7 +31,7 @@ class MagnifierCore extends StatefulWidget { this.onTap, this.onDoubleTap, required this.child, - }) : super(key: key); + }); @override State createState() => _MagnifierCoreState(); @@ -197,11 +197,12 @@ class _MagnifierCoreState extends State with TickerProviderStateM } void onTap(TapUpDetails details) { - if (widget.onTap == null) return; + final onTap = widget.onTap; + if (onTap == null) return; final viewportTapPosition = details.localPosition; final childTapPosition = scaleBoundaries.viewportToChildPosition(controller, viewportTapPosition); - widget.onTap!.call(context, details, controller.currentState, childTapPosition); + onTap(context, details, controller.currentState, childTapPosition); } void onDoubleTap(TapDownDetails details) { diff --git a/lib/widgets/common/magnifier/core/gesture_detector.dart b/lib/widgets/common/magnifier/core/gesture_detector.dart index 507ae5252..58117dd1e 100644 --- a/lib/widgets/common/magnifier/core/gesture_detector.dart +++ b/lib/widgets/common/magnifier/core/gesture_detector.dart @@ -3,10 +3,11 @@ import 'package:aves/widgets/common/magnifier/pan/corner_hit_detector.dart'; import 'package:aves/widgets/common/magnifier/pan/gesture_detector_scope.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; class MagnifierGestureDetector extends StatefulWidget { const MagnifierGestureDetector({ - Key? key, + super.key, required this.hitDetector, this.onScaleStart, this.onScaleUpdate, @@ -16,7 +17,7 @@ class MagnifierGestureDetector extends StatefulWidget { this.onDoubleTap, this.behavior, this.child, - }) : super(key: key); + }); final CornerHitDetector hitDetector; final void Function(ScaleStartDetails details, bool doubleTap)? onScaleStart; @@ -39,6 +40,7 @@ class _MagnifierGestureDetectorState extends State { @override Widget build(BuildContext context) { + final gestureSettings = context.select((mq) => mq.gestureSettings); final gestures = {}; if (widget.onTapDown != null || widget.onTapUp != null) { @@ -63,9 +65,11 @@ class _MagnifierGestureDetectorState extends State { doubleTapDetails: doubleTapDetails, ), (instance) { - instance.onStart = widget.onScaleStart != null ? (details) => widget.onScaleStart!(details, doubleTapDetails.value != null) : null; - instance.onUpdate = widget.onScaleUpdate; - instance.onEnd = widget.onScaleEnd; + instance + ..onStart = widget.onScaleStart != null ? (details) => widget.onScaleStart!(details, doubleTapDetails.value != null) : null + ..onUpdate = widget.onScaleUpdate + ..onEnd = widget.onScaleEnd + ..gestureSettings = gestureSettings; }, ); } @@ -73,14 +77,16 @@ class _MagnifierGestureDetectorState extends State { gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => DoubleTapGestureRecognizer(debugOwner: this), (instance) { - instance.onDoubleTapCancel = () => doubleTapDetails.value = null; - instance.onDoubleTapDown = (details) => doubleTapDetails.value = details; - instance.onDoubleTap = widget.onDoubleTap != null - ? () { - widget.onDoubleTap!(doubleTapDetails.value!); - doubleTapDetails.value = null; - } - : null; + final onDoubleTap = widget.onDoubleTap; + instance + ..onDoubleTapCancel = _onDoubleTapCancel + ..onDoubleTapDown = _onDoubleTapDown + ..onDoubleTap = onDoubleTap != null + ? () { + onDoubleTap(doubleTapDetails.value!); + doubleTapDetails.value = null; + } + : null; }, ); @@ -90,4 +96,8 @@ class _MagnifierGestureDetectorState extends State { child: widget.child, ); } + + void _onDoubleTapCancel() => doubleTapDetails.value = null; + + void _onDoubleTapDown(TapDownDetails details) => doubleTapDetails.value = details; } diff --git a/lib/widgets/common/magnifier/core/scale_gesture_recognizer.dart b/lib/widgets/common/magnifier/core/scale_gesture_recognizer.dart index 05cb3bfc3..d08b84c75 100644 --- a/lib/widgets/common/magnifier/core/scale_gesture_recognizer.dart +++ b/lib/widgets/common/magnifier/core/scale_gesture_recognizer.dart @@ -11,12 +11,12 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { final ValueNotifier doubleTapDetails; MagnifierGestureRecognizer({ - Object? debugOwner, + super.debugOwner, required this.hitDetector, required this.validateAxis, this.touchSlopFactor = 2, required this.doubleTapDetails, - }) : super(debugOwner: debugOwner); + }); Map _pointerLocations = {}; @@ -128,14 +128,16 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { final doubleTap = doubleTapDetails.value != null; if (shouldMove || doubleTap) { + final pointerDeviceKind = event.kind; final spanDelta = (_currentSpan! - _initialSpan!).abs(); final focalPointDelta = (_currentFocalPoint! - _initialFocalPoint!).distance; // warning: do not compare `focalPointDelta` to `kPanSlop` - // `ScaleGestureRecognizer` uses `kPanSlop`, but `HorizontalDragGestureRecognizer` uses `kTouchSlop` + // `ScaleGestureRecognizer` uses `kPanSlop` (or platform settings, cf gestures/events.dart `computePanSlop`), + // but `HorizontalDragGestureRecognizer` uses `kTouchSlop` (or platform settings, cf gestures/events.dart `computeHitSlop`) // and the magnifier recognizer may compete with the `HorizontalDragGestureRecognizer` from a containing `PageView` // setting `touchSlopFactor` to 2 restores default `ScaleGestureRecognizer` behaviour as `kPanSlop = kTouchSlop * 2.0` // setting `touchSlopFactor` in [0, 1] will allow this recognizer to accept the gesture before the one from `PageView` - if (spanDelta > kScaleSlop || focalPointDelta > kTouchSlop * touchSlopFactor) { + if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computeHitSlop(pointerDeviceKind, gestureSettings) * touchSlopFactor) { acceptGesture(event.pointer); } } diff --git a/lib/widgets/common/magnifier/magnifier.dart b/lib/widgets/common/magnifier/magnifier.dart index 0661d557b..98d5b6d35 100644 --- a/lib/widgets/common/magnifier/magnifier.dart +++ b/lib/widgets/common/magnifier/magnifier.dart @@ -20,7 +20,7 @@ import 'package:flutter/material.dart'; */ class Magnifier extends StatelessWidget { const Magnifier({ - Key? key, + super.key, required this.controller, required this.childSize, this.minScale = const ScaleLevel(factor: .0), @@ -31,7 +31,7 @@ class Magnifier extends StatelessWidget { this.onTap, this.onDoubleTap, required this.child, - }) : super(key: key); + }); final MagnifierController controller; diff --git a/lib/widgets/common/magnifier/pan/corner_hit_detector.dart b/lib/widgets/common/magnifier/pan/corner_hit_detector.dart index 604666b7e..138a5c8df 100644 --- a/lib/widgets/common/magnifier/pan/corner_hit_detector.dart +++ b/lib/widgets/common/magnifier/pan/corner_hit_detector.dart @@ -3,8 +3,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; mixin CornerHitDetector on MagnifierControllerDelegate { - _AxisHit hitAxis() => _AxisHit(_hitCornersX(), _hitCornersY()); - // the child width/height is not accurate for some image size & scale combos // e.g. 3580.0 * 0.1005586592178771 yields 360.0 // but 4764.0 * 0.07556675062972293 yields 360.00000000000006 @@ -53,17 +51,6 @@ mixin CornerHitDetector on MagnifierControllerDelegate { } } -class _AxisHit { - _AxisHit(this.hasHitX, this.hasHitY); - - final _CornerHit hasHitX; - final _CornerHit hasHitY; - - bool get hasHitAny => hasHitX.hasHitAny || hasHitY.hasHitAny; - - bool get hasHitBoth => hasHitX.hasHitBoth && hasHitY.hasHitBoth; -} - class _CornerHit { const _CornerHit(this.hasHitMin, this.hasHitMax); diff --git a/lib/widgets/common/magnifier/pan/gesture_detector_scope.dart b/lib/widgets/common/magnifier/pan/gesture_detector_scope.dart index 9763bc1f6..e7a597baf 100644 --- a/lib/widgets/common/magnifier/pan/gesture_detector_scope.dart +++ b/lib/widgets/common/magnifier/pan/gesture_detector_scope.dart @@ -8,11 +8,11 @@ import 'package:flutter/widgets.dart'; /// such as [PageView], [Dismissible], [BottomSheet]. class MagnifierGestureDetectorScope extends InheritedWidget { const MagnifierGestureDetectorScope({ - Key? key, + super.key, required this.axis, this.touchSlopFactor = .8, required Widget child, - }) : super(key: key, child: child); + }) : super(child: child); static MagnifierGestureDetectorScope? of(BuildContext context) { final scope = context.dependOnInheritedWidgetOfExactType(); diff --git a/lib/widgets/common/magnifier/pan/scroll_physics.dart b/lib/widgets/common/magnifier/pan/scroll_physics.dart index 61d3492ec..ca4f12137 100644 --- a/lib/widgets/common/magnifier/pan/scroll_physics.dart +++ b/lib/widgets/common/magnifier/pan/scroll_physics.dart @@ -1,29 +1,34 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; -// `PageView` contains a `Scrollable` which sets up a `HorizontalDragGestureRecognizer` -// this recognizer will win in the gesture arena when the drag distance reaches `kTouchSlop` -// we cannot change that, but we can prevent the scrollable from panning until this threshold is reached -// and let other recognizers accept the gesture instead +// `PageView` contains a `Scrollable` which sets up a `HorizontalDragGestureRecognizer`. +// This recognizer will win in the gesture arena when the drag distance reaches +// `kTouchSlop` (or platform settings, cf gestures/events.dart `computeHitSlop`). +// We cannot change that, but we can prevent the scrollable from panning until this threshold is reached +// and let other recognizers accept the gesture instead. class MagnifierScrollerPhysics extends ScrollPhysics { - const MagnifierScrollerPhysics({ - this.touchSlopFactor = 1, - ScrollPhysics? parent, - }) : super(parent: parent); + final DeviceGestureSettings? gestureSettings; // in [0, 1] // 0: most reactive but will not let Magnifier recognizers accept gestures // 1: less reactive but gives the most leeway to Magnifier recognizers final double touchSlopFactor; + const MagnifierScrollerPhysics({ + required this.gestureSettings, + this.touchSlopFactor = 1, + ScrollPhysics? parent, + }) : super(parent: parent); + @override MagnifierScrollerPhysics applyTo(ScrollPhysics? ancestor) { return MagnifierScrollerPhysics( + gestureSettings: gestureSettings, touchSlopFactor: touchSlopFactor, parent: buildParent(ancestor), ); } @override - double get dragStartDistanceMotionThreshold => kTouchSlop * touchSlopFactor; + double get dragStartDistanceMotionThreshold => (gestureSettings?.touchSlop ?? kTouchSlop) * touchSlopFactor; } diff --git a/lib/widgets/common/map/attribution.dart b/lib/widgets/common/map/attribution.dart index 9488b38cb..57aaab097 100644 --- a/lib/widgets/common/map/attribution.dart +++ b/lib/widgets/common/map/attribution.dart @@ -1,6 +1,6 @@ -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/info/common.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -9,9 +9,9 @@ class Attribution extends StatelessWidget { final EntryMapStyle style; const Attribution({ - Key? key, + super.key, required this.style, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -38,8 +38,11 @@ class Attribution extends StatelessWidget { p: theme.textTheme.caption!.merge(const TextStyle(fontSize: InfoRowGroup.fontSize)), ), onTapLink: (text, href, title) async { - if (href != null && await canLaunch(href)) { - await launch(href); + if (href != null) { + final url = Uri.parse(href); + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } } }, ), diff --git a/lib/widgets/common/map/buttons.dart b/lib/widgets/common/map/buttons.dart deleted file mode 100644 index 976f5561b..000000000 --- a/lib/widgets/common/map/buttons.dart +++ /dev/null @@ -1,331 +0,0 @@ -import 'package:aves/model/filters/coordinate.dart'; -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/settings/enums/map_style.dart'; -import 'package:aves/model/settings/settings.dart'; -import 'package:aves/services/common/services.dart'; -import 'package:aves/theme/durations.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:aves/theme/themes.dart'; -import 'package:aves/utils/debouncer.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/fx/blurred.dart'; -import 'package:aves/widgets/common/fx/borders.dart'; -import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; -import 'package:aves/widgets/common/map/compass.dart'; -import 'package:aves/widgets/common/map/theme.dart'; -import 'package:aves/widgets/common/map/zoomed_bounds.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; -import 'package:aves/widgets/viewer/notifications.dart'; -import 'package:flutter/material.dart'; -import 'package:latlong2/latlong.dart'; -import 'package:provider/provider.dart'; - -typedef MapOpener = void Function(BuildContext context); - -class MapButtonPanel extends StatelessWidget { - final ValueNotifier boundsNotifier; - final Future Function(double amount)? zoomBy; - final MapOpener? openMapPage; - final VoidCallback? resetRotation; - - const MapButtonPanel({ - Key? key, - required this.boundsNotifier, - this.zoomBy, - this.openMapPage, - this.resetRotation, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final iconTheme = IconTheme.of(context); - final iconSize = Size.square(iconTheme.size!); - - Widget? navigationButton; - switch (context.select((v) => v.navigationButton)) { - case MapNavigationButton.back: - navigationButton = MapOverlayButton( - icon: const BackButtonIcon(), - onPressed: () => Navigator.pop(context), - tooltip: MaterialLocalizations.of(context).backButtonTooltip, - ); - break; - case MapNavigationButton.map: - if (openMapPage != null) { - navigationButton = MapOverlayButton( - icon: const Icon(AIcons.map), - onPressed: () => openMapPage?.call(context), - tooltip: context.l10n.openMapPageTooltip, - ); - } - break; - } - - final showCoordinateFilter = context.select((v) => v.showCoordinateFilter); - final visualDensity = context.select((v) => v.visualDensity); - final double padding = visualDensity == VisualDensity.compact ? 4 : 8; - - return Positioned.fill( - child: TooltipTheme( - data: TooltipTheme.of(context).copyWith( - preferBelow: false, - ), - child: SafeArea( - bottom: false, - child: Stack( - children: [ - Positioned( - left: padding, - right: padding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(top: padding), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (navigationButton != null) ...[ - navigationButton, - SizedBox(height: padding), - ], - ValueListenableBuilder( - valueListenable: boundsNotifier, - builder: (context, bounds, child) { - final degrees = bounds.rotation; - final opacity = degrees == 0 ? .0 : 1.0; - final animationDuration = context.select((v) => v.viewerOverlayAnimation); - return IgnorePointer( - ignoring: opacity == 0, - child: AnimatedOpacity( - opacity: opacity, - duration: animationDuration, - child: MapOverlayButton( - icon: Transform( - origin: iconSize.center(Offset.zero), - transform: Matrix4.rotationZ(degToRadian(degrees)), - child: CustomPaint( - painter: CompassPainter( - color: iconTheme.color!, - ), - size: iconSize, - ), - ), - onPressed: () => resetRotation?.call(), - tooltip: context.l10n.mapPointNorthUpTooltip, - ), - ), - ); - }, - ), - ], - ), - ), - showCoordinateFilter - ? Expanded( - child: _OverlayCoordinateFilterChip( - boundsNotifier: boundsNotifier, - padding: padding, - ), - ) - : const Spacer(), - Padding( - padding: EdgeInsets.only(top: padding), - child: MapOverlayButton( - icon: const Icon(AIcons.layers), - onPressed: () async { - final canUseGoogleMaps = await availability.canUseGoogleMaps; - final availableStyles = EntryMapStyle.values.where((style) => !style.isGoogleMaps || canUseGoogleMaps); - final preferredStyle = settings.infoMapStyle; - final initialStyle = availableStyles.contains(preferredStyle) ? preferredStyle : availableStyles.first; - await showSelectionDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: initialStyle, - options: Map.fromEntries(availableStyles.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.mapStyleTitle, - ), - onSelection: (v) => settings.infoMapStyle = v, - ); - }, - tooltip: context.l10n.mapStyleTooltip, - ), - ), - ], - ), - ), - Positioned( - right: padding, - bottom: padding, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - MapOverlayButton( - icon: const Icon(AIcons.zoomIn), - onPressed: zoomBy != null ? () => zoomBy?.call(1) : null, - tooltip: context.l10n.mapZoomInTooltip, - ), - SizedBox(height: padding), - MapOverlayButton( - icon: const Icon(AIcons.zoomOut), - onPressed: zoomBy != null ? () => zoomBy?.call(-1) : null, - tooltip: context.l10n.mapZoomOutTooltip, - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} - -class MapOverlayButton extends StatelessWidget { - final Widget icon; - final String tooltip; - final VoidCallback? onPressed; - - const MapOverlayButton({ - Key? key, - required this.icon, - required this.tooltip, - required this.onPressed, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final blurred = settings.enableOverlayBlurEffect; - return Selector>( - selector: (context, v) => v.scale, - builder: (context, scale, child) => ScaleTransition( - scale: scale, - child: child, - ), - child: BlurredOval( - enabled: blurred, - child: Material( - type: MaterialType.circle, - color: Themes.overlayBackgroundColor(brightness: Theme.of(context).brightness, blurred: blurred), - child: Ink( - decoration: BoxDecoration( - border: AvesBorder.border(context), - shape: BoxShape.circle, - ), - child: Selector( - selector: (context, v) => v.visualDensity, - builder: (context, visualDensity, child) => IconButton( - iconSize: 20, - visualDensity: visualDensity, - icon: icon, - onPressed: onPressed, - tooltip: tooltip, - ), - ), - ), - ), - ), - ); - } -} - -class _OverlayCoordinateFilterChip extends StatefulWidget { - final ValueNotifier boundsNotifier; - final double padding; - - const _OverlayCoordinateFilterChip({ - Key? key, - required this.boundsNotifier, - required this.padding, - }) : super(key: key); - - @override - State<_OverlayCoordinateFilterChip> createState() => _OverlayCoordinateFilterChipState(); -} - -class _OverlayCoordinateFilterChipState extends State<_OverlayCoordinateFilterChip> { - final Debouncer _debouncer = Debouncer(delay: Durations.mapInfoDebounceDelay); - final ValueNotifier _idleBoundsNotifier = ValueNotifier(null); - - @override - void initState() { - super.initState(); - _registerWidget(widget); - } - - @override - void didUpdateWidget(covariant _OverlayCoordinateFilterChip oldWidget) { - super.didUpdateWidget(oldWidget); - _unregisterWidget(oldWidget); - _registerWidget(widget); - } - - @override - void dispose() { - _unregisterWidget(widget); - super.dispose(); - } - - void _registerWidget(_OverlayCoordinateFilterChip widget) { - widget.boundsNotifier.addListener(_onBoundsChanged); - } - - void _unregisterWidget(_OverlayCoordinateFilterChip widget) { - widget.boundsNotifier.removeListener(_onBoundsChanged); - } - - @override - Widget build(BuildContext context) { - final blurred = settings.enableOverlayBlurEffect; - final theme = Theme.of(context); - return Theme( - data: theme.copyWith( - scaffoldBackgroundColor: Themes.overlayBackgroundColor(brightness: theme.brightness, blurred: blurred), - ), - child: Align( - alignment: Alignment.topLeft, - child: Selector>( - selector: (context, v) => v.scale, - builder: (context, scale, child) => SizeTransition( - sizeFactor: scale, - axisAlignment: 1, - child: FadeTransition( - opacity: scale, - child: child, - ), - ), - child: ValueListenableBuilder( - valueListenable: _idleBoundsNotifier, - builder: (context, bounds, child) { - if (bounds == null) return const SizedBox(); - final filter = CoordinateFilter( - bounds.sw, - bounds.ne, - // more stable format when bounds change - minuteSecondPadding: true, - ); - return Padding( - padding: EdgeInsets.all(widget.padding), - child: BlurredRRect.all( - enabled: blurred, - borderRadius: AvesFilterChip.defaultRadius, - child: AvesFilterChip( - filter: filter, - useFilterColor: false, - maxWidth: double.infinity, - onTap: (filter) => FilterSelectedNotification(CoordinateFilter(bounds.sw, bounds.ne)).dispatch(context), - ), - ), - ); - }, - ), - ), - ), - ); - } - - void _onBoundsChanged() { - _debouncer(() => _idleBoundsNotifier.value = widget.boundsNotifier.value); - } -} diff --git a/lib/widgets/common/map/buttons/button.dart b/lib/widgets/common/map/buttons/button.dart new file mode 100644 index 000000000..7bd435ce8 --- /dev/null +++ b/lib/widgets/common/map/buttons/button.dart @@ -0,0 +1,55 @@ +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/themes.dart'; +import 'package:aves/widgets/common/fx/blurred.dart'; +import 'package:aves/widgets/common/fx/borders.dart'; +import 'package:aves_map/aves_map.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class MapOverlayButton extends StatelessWidget { + final Widget icon; + final String tooltip; + final VoidCallback? onPressed; + + const MapOverlayButton({ + super.key, + required this.icon, + required this.tooltip, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + final blurred = settings.enableOverlayBlurEffect; + return Selector>( + selector: (context, v) => v.scale, + builder: (context, scale, child) => ScaleTransition( + scale: scale, + child: child, + ), + child: BlurredOval( + enabled: blurred, + child: Material( + type: MaterialType.circle, + color: Themes.overlayBackgroundColor(brightness: Theme.of(context).brightness, blurred: blurred), + child: Ink( + decoration: BoxDecoration( + border: AvesBorder.border(context), + shape: BoxShape.circle, + ), + child: Selector( + selector: (context, v) => v.visualDensity, + builder: (context, visualDensity, child) => IconButton( + iconSize: 20, + visualDensity: visualDensity, + icon: icon, + onPressed: onPressed, + tooltip: tooltip, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/common/map/buttons/coordinate_filter.dart b/lib/widgets/common/map/buttons/coordinate_filter.dart new file mode 100644 index 000000000..3056ace31 --- /dev/null +++ b/lib/widgets/common/map/buttons/coordinate_filter.dart @@ -0,0 +1,111 @@ +import 'package:aves/model/filters/coordinate.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/themes.dart'; +import 'package:aves/utils/debouncer.dart'; +import 'package:aves/widgets/common/fx/blurred.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:aves/widgets/viewer/notifications.dart'; +import 'package:aves_map/aves_map.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class OverlayCoordinateFilterChip extends StatefulWidget { + final ValueNotifier boundsNotifier; + final double padding; + + const OverlayCoordinateFilterChip({ + super.key, + required this.boundsNotifier, + required this.padding, + }); + + @override + State createState() => _OverlayCoordinateFilterChipState(); +} + +class _OverlayCoordinateFilterChipState extends State { + final Debouncer _debouncer = Debouncer(delay: Durations.mapInfoDebounceDelay); + final ValueNotifier _idleBoundsNotifier = ValueNotifier(null); + + @override + void initState() { + super.initState(); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant OverlayCoordinateFilterChip oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(OverlayCoordinateFilterChip widget) { + widget.boundsNotifier.addListener(_onBoundsChanged); + } + + void _unregisterWidget(OverlayCoordinateFilterChip widget) { + widget.boundsNotifier.removeListener(_onBoundsChanged); + } + + @override + Widget build(BuildContext context) { + final blurred = settings.enableOverlayBlurEffect; + final theme = Theme.of(context); + return Theme( + data: theme.copyWith( + scaffoldBackgroundColor: Themes.overlayBackgroundColor(brightness: theme.brightness, blurred: blurred), + ), + child: Align( + alignment: Alignment.topLeft, + child: Selector>( + selector: (context, v) => v.scale, + builder: (context, scale, child) => SizeTransition( + sizeFactor: scale, + axisAlignment: 1, + child: FadeTransition( + opacity: scale, + child: child, + ), + ), + child: ValueListenableBuilder( + valueListenable: _idleBoundsNotifier, + builder: (context, bounds, child) { + if (bounds == null) return const SizedBox(); + final filter = CoordinateFilter( + bounds.sw, + bounds.ne, + // more stable format when bounds change + minuteSecondPadding: true, + ); + return Padding( + padding: EdgeInsets.all(widget.padding), + child: BlurredRRect.all( + enabled: blurred, + borderRadius: AvesFilterChip.defaultRadius, + child: AvesFilterChip( + filter: filter, + useFilterColor: false, + maxWidth: double.infinity, + onTap: (filter) => FilterSelectedNotification(CoordinateFilter(bounds.sw, bounds.ne)).dispatch(context), + ), + ), + ); + }, + ), + ), + ), + ); + } + + void _onBoundsChanged() { + _debouncer(() => _idleBoundsNotifier.value = widget.boundsNotifier.value); + } +} diff --git a/lib/widgets/common/map/buttons/panel.dart b/lib/widgets/common/map/buttons/panel.dart new file mode 100644 index 000000000..62377021b --- /dev/null +++ b/lib/widgets/common/map/buttons/panel.dart @@ -0,0 +1,168 @@ +import 'package:aves/model/settings/enums/map_style.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/map/buttons/button.dart'; +import 'package:aves/widgets/common/map/buttons/coordinate_filter.dart'; +import 'package:aves/widgets/common/map/compass.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves_map/aves_map.dart'; +import 'package:flutter/material.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:provider/provider.dart'; + +class MapButtonPanel extends StatelessWidget { + final ValueNotifier boundsNotifier; + final Future Function(double amount)? zoomBy; + final void Function(BuildContext context)? openMapPage; + final VoidCallback? resetRotation; + + const MapButtonPanel({ + super.key, + required this.boundsNotifier, + this.zoomBy, + this.openMapPage, + this.resetRotation, + }); + + @override + Widget build(BuildContext context) { + final iconTheme = IconTheme.of(context); + final iconSize = Size.square(iconTheme.size!); + + Widget? navigationButton; + switch (context.select((v) => v.navigationButton)) { + case MapNavigationButton.back: + navigationButton = MapOverlayButton( + icon: const BackButtonIcon(), + onPressed: () => Navigator.pop(context), + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + ); + break; + case MapNavigationButton.map: + if (openMapPage != null) { + navigationButton = MapOverlayButton( + icon: const Icon(AIcons.map), + onPressed: () => openMapPage?.call(context), + tooltip: context.l10n.openMapPageTooltip, + ); + } + break; + } + + final showCoordinateFilter = context.select((v) => v.showCoordinateFilter); + final visualDensity = context.select((v) => v.visualDensity); + final double padding = visualDensity == VisualDensity.compact ? 4 : 8; + + return Positioned.fill( + child: TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, + ), + child: SafeArea( + bottom: false, + child: Stack( + children: [ + Positioned( + left: padding, + right: padding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: padding), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (navigationButton != null) ...[ + navigationButton, + SizedBox(height: padding), + ], + ValueListenableBuilder( + valueListenable: boundsNotifier, + builder: (context, bounds, child) { + final degrees = bounds.rotation; + final opacity = degrees == 0 ? .0 : 1.0; + final animationDuration = context.select((v) => v.viewerOverlayAnimation); + return IgnorePointer( + ignoring: opacity == 0, + child: AnimatedOpacity( + opacity: opacity, + duration: animationDuration, + child: MapOverlayButton( + icon: Transform( + origin: iconSize.center(Offset.zero), + transform: Matrix4.rotationZ(degToRadian(degrees)), + child: CustomPaint( + painter: CompassPainter( + color: iconTheme.color!, + ), + size: iconSize, + ), + ), + onPressed: () => resetRotation?.call(), + tooltip: context.l10n.mapPointNorthUpTooltip, + ), + ), + ); + }, + ), + ], + ), + ), + showCoordinateFilter + ? Expanded( + child: OverlayCoordinateFilterChip( + boundsNotifier: boundsNotifier, + padding: padding, + ), + ) + : const Spacer(), + Padding( + padding: EdgeInsets.only(top: padding), + child: MapOverlayButton( + icon: const Icon(AIcons.layers), + onPressed: () => showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: settings.infoMapStyle, + options: Map.fromEntries(availability.mapStyles.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.mapStyleTitle, + ), + onSelection: (v) => settings.infoMapStyle = v, + ), + tooltip: context.l10n.mapStyleTooltip, + ), + ), + ], + ), + ), + Positioned( + right: padding, + bottom: padding, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + MapOverlayButton( + icon: const Icon(AIcons.zoomIn), + onPressed: zoomBy != null ? () => zoomBy?.call(1) : null, + tooltip: context.l10n.mapZoomInTooltip, + ), + SizedBox(height: padding), + MapOverlayButton( + icon: const Icon(AIcons.zoomOut), + onPressed: zoomBy != null ? () => zoomBy?.call(-1) : null, + tooltip: context.l10n.mapZoomOutTooltip, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/common/map/decorator.dart b/lib/widgets/common/map/decorator.dart index cafee9b0e..7969420ae 100644 --- a/lib/widgets/common/map/decorator.dart +++ b/lib/widgets/common/map/decorator.dart @@ -1,5 +1,5 @@ import 'package:aves/widgets/common/fx/borders.dart'; -import 'package:aves/widgets/common/map/theme.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -11,9 +11,9 @@ class MapDecorator extends StatelessWidget { static const mapLoadingGrid = Color(0xFFC4BEBB); const MapDecorator({ - Key? key, + super.key, this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart index 8eeb720bc..44de80547 100644 --- a/lib/widgets/common/map/geo_map.dart +++ b/lib/widgets/common/map/geo_map.dart @@ -2,26 +2,21 @@ import 'dart:async'; import 'dart:math'; import 'package:aves/model/entry.dart'; -import 'package:aves/model/geotiff.dart'; -import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:aves/widgets/common/map/attribution.dart'; -import 'package:aves/widgets/common/map/buttons.dart'; -import 'package:aves/widgets/common/map/controller.dart'; +import 'package:aves/widgets/common/map/buttons/panel.dart'; import 'package:aves/widgets/common/map/decorator.dart'; -import 'package:aves/widgets/common/map/geo_entry.dart'; -import 'package:aves/widgets/common/map/google/map.dart'; import 'package:aves/widgets/common/map/leaflet/map.dart'; -import 'package:aves/widgets/common/map/marker.dart'; -import 'package:aves/widgets/common/map/theme.dart'; -import 'package:aves/widgets/common/map/zoomed_bounds.dart'; +import 'package:aves/widgets/common/thumbnail/image.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:collection/collection.dart'; -import 'package:equatable/equatable.dart'; import 'package:fluster/fluster.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; @@ -35,17 +30,14 @@ class GeoMap extends StatefulWidget { final ValueNotifier isAnimatingNotifier; final ValueNotifier? dotLocationNotifier; final ValueNotifier? overlayOpacityNotifier; - final MappedGeoTiff? overlayEntry; + final MapOverlay? overlayEntry; final UserZoomChangeCallback? onUserZoomChange; - final void Function(LatLng location)? onMapTap; - final MarkerTapCallback? onMarkerTap; - final MapOpener? openMapPage; - - static const markerImageExtent = 48.0; - static const markerArrowSize = Size(8, 6); + final MapTapCallback? onMapTap; + final void Function(LatLng averageLocation, AvesEntry markerEntry, Set Function() getClusterEntries)? onMarkerTap; + final void Function(BuildContext context)? openMapPage; const GeoMap({ - Key? key, + super.key, this.controller, this.collectionListenable, required this.entries, @@ -58,7 +50,7 @@ class GeoMap extends StatefulWidget { this.onMapTap, this.onMarkerTap, this.openMapPage, - }) : super(key: key); + }); @override State createState() => _GeoMapState(); @@ -71,10 +63,10 @@ class _GeoMapState extends State { // cf https://github.com/flutter/flutter/issues/28493 // it is especially severe the first time, but still significant afterwards // so we prevent loading it while scrolling or animating - bool _googleMapsLoaded = false; + bool _heavyMapLoaded = false; late final ValueNotifier _boundsNotifier; - Fluster? _defaultMarkerCluster; - Fluster? _slowMarkerCluster; + Fluster>? _defaultMarkerCluster; + Fluster>? _slowMarkerCluster; final AChangeNotifier _clusterChangeNotifier = AChangeNotifier(); List get entries => widget.entries; @@ -121,7 +113,7 @@ class _GeoMapState extends State { @override Widget build(BuildContext context) { - void _onMarkerTap(GeoEntry geoEntry) { + void _onMarkerTap(GeoEntry geoEntry) { final onTap = widget.onMarkerTap; if (onTap == null) return; @@ -159,60 +151,74 @@ class _GeoMapState extends State { return Selector( selector: (context, s) => s.infoMapStyle, builder: (context, mapStyle, child) { - final isGoogleMaps = mapStyle.isGoogleMaps; - final progressive = !isGoogleMaps; - Widget _buildMarkerWidget(MarkerKey key) => ImageMarker( + final isHeavy = mapStyle.isHeavy; + Widget _buildMarkerWidget(MarkerKey key) => ImageMarker( key: key, - entry: key.entry, count: key.count, - extent: GeoMap.markerImageExtent, - arrowSize: GeoMap.markerArrowSize, - progressive: progressive, + buildThumbnailImage: (extent) => ThumbnailImage( + entry: key.entry, + extent: extent, + progressive: !isHeavy, + ), ); + bool _isMarkerImageReady(MarkerKey key) => key.entry.isThumbnailReady(extent: MapThemeData.markerImageExtent); - Widget child = isGoogleMaps - ? EntryGoogleMap( - controller: widget.controller, - clusterListenable: _clusterChangeNotifier, - boundsNotifier: _boundsNotifier, - minZoom: 0, - maxZoom: 20, - style: mapStyle, - markerClusterBuilder: _buildMarkerClusters, - markerWidgetBuilder: _buildMarkerWidget, - dotLocationNotifier: widget.dotLocationNotifier, - overlayOpacityNotifier: widget.overlayOpacityNotifier, - overlayEntry: widget.overlayEntry, - onUserZoomChange: widget.onUserZoomChange, - onMapTap: widget.onMapTap, - onMarkerTap: _onMarkerTap, - openMapPage: widget.openMapPage, - ) - : EntryLeafletMap( - controller: widget.controller, - clusterListenable: _clusterChangeNotifier, - boundsNotifier: _boundsNotifier, - minZoom: 2, - maxZoom: 16, - style: mapStyle, - markerClusterBuilder: _buildMarkerClusters, - markerWidgetBuilder: _buildMarkerWidget, - dotLocationNotifier: widget.dotLocationNotifier, - markerSize: Size( - GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2, - GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2 + GeoMap.markerArrowSize.height, - ), - dotMarkerSize: const Size( - DotMarker.diameter + ImageMarker.outerBorderWidth * 2, - DotMarker.diameter + ImageMarker.outerBorderWidth * 2, - ), - overlayOpacityNotifier: widget.overlayOpacityNotifier, - overlayEntry: widget.overlayEntry, - onUserZoomChange: widget.onUserZoomChange, - onMapTap: widget.onMapTap, - onMarkerTap: _onMarkerTap, - openMapPage: widget.openMapPage, - ); + Widget child = const SizedBox(); + switch (mapStyle) { + case EntryMapStyle.googleNormal: + case EntryMapStyle.googleHybrid: + case EntryMapStyle.googleTerrain: + case EntryMapStyle.hmsNormal: + case EntryMapStyle.hmsTerrain: + child = mobileServices.buildMap( + controller: widget.controller, + clusterListenable: _clusterChangeNotifier, + boundsNotifier: _boundsNotifier, + style: mapStyle, + decoratorBuilder: _decorateMap, + buttonPanelBuilder: _buildButtonPanel, + markerClusterBuilder: _buildMarkerClusters, + markerWidgetBuilder: _buildMarkerWidget, + markerImageReadyChecker: _isMarkerImageReady, + dotLocationNotifier: widget.dotLocationNotifier, + overlayOpacityNotifier: widget.overlayOpacityNotifier, + overlayEntry: widget.overlayEntry, + onUserZoomChange: widget.onUserZoomChange, + onMapTap: widget.onMapTap, + onMarkerTap: _onMarkerTap, + ); + break; + case EntryMapStyle.osmHot: + case EntryMapStyle.stamenToner: + case EntryMapStyle.stamenWatercolor: + child = EntryLeafletMap( + controller: widget.controller, + clusterListenable: _clusterChangeNotifier, + boundsNotifier: _boundsNotifier, + minZoom: 2, + maxZoom: 16, + style: mapStyle, + decoratorBuilder: _decorateMap, + buttonPanelBuilder: _buildButtonPanel, + markerClusterBuilder: _buildMarkerClusters, + markerWidgetBuilder: _buildMarkerWidget, + dotLocationNotifier: widget.dotLocationNotifier, + markerSize: Size( + MapThemeData.markerImageExtent + MapThemeData.markerOuterBorderWidth * 2, + MapThemeData.markerImageExtent + MapThemeData.markerOuterBorderWidth * 2 + MapThemeData.markerArrowSize.height, + ), + dotMarkerSize: const Size( + DotMarker.diameter + MapThemeData.markerOuterBorderWidth * 2, + DotMarker.diameter + MapThemeData.markerOuterBorderWidth * 2, + ), + overlayOpacityNotifier: widget.overlayOpacityNotifier, + overlayEntry: widget.overlayEntry, + onUserZoomChange: widget.onUserZoomChange, + onMapTap: widget.onMapTap, + onMarkerTap: _onMarkerTap, + ); + break; + } final mapHeight = context.select((v) => v.mapHeight); child = Column( @@ -239,8 +245,8 @@ class _GeoMapState extends State { child: ValueListenableBuilder( valueListenable: widget.isAnimatingNotifier, builder: (context, animating, child) { - if (!animating && isGoogleMaps) { - _googleMapsLoaded = true; + if (!animating && isHeavy) { + _heavyMapLoaded = true; } Widget replacement = Stack( children: [ @@ -258,7 +264,7 @@ class _GeoMapState extends State { ); } return Visibility( - visible: !isGoogleMaps || _googleMapsLoaded, + visible: !isHeavy || _heavyMapLoaded, replacement: replacement, child: child!, ); @@ -303,10 +309,10 @@ class _GeoMapState extends State { _clusterChangeNotifier.notify(); } - Fluster _buildFluster({int nodeSize = 64}) { + Fluster> _buildFluster({int nodeSize = 64}) { final markers = entries.map((entry) { final latLng = entry.latLng!; - return GeoEntry( + return GeoEntry( entry: entry, latitude: latLng.latitude, longitude: latLng.longitude, @@ -314,7 +320,7 @@ class _GeoMapState extends State { ); }).toList(); - return Fluster( + return Fluster>( // we keep clustering on the whole range of zooms (including the maximum) // to avoid collocated entries overlapping minZoom: 0, @@ -329,11 +335,11 @@ class _GeoMapState extends State { // use lambda instead of tear-off because of runtime exception when using // `T Function(BaseCluster, double, double)` for `T Function(BaseCluster?, double?, double?)` // ignore: unnecessary_lambdas - createCluster: (base, lng, lat) => GeoEntry.createCluster(base, lng, lat), + createCluster: (base, lng, lat) => GeoEntry.createCluster(base, lng, lat), ); } - Map _buildMarkerClusters() { + Map, GeoEntry> _buildMarkerClusters() { final bounds = _boundsNotifier.value; final geoEntries = _defaultMarkerCluster?.clusters(bounds.boundingBox, bounds.zoom.round()) ?? []; return Map.fromEntries(geoEntries.map((v) { @@ -345,20 +351,17 @@ class _GeoMapState extends State { return MapEntry(MarkerKey(v.entry!, null), v); })); } + + Widget _decorateMap(BuildContext context, Widget? child) => MapDecorator(child: child); + + Widget _buildButtonPanel( + Future Function(double amount) zoomBy, + VoidCallback resetRotation, + ) => + MapButtonPanel( + boundsNotifier: _boundsNotifier, + zoomBy: zoomBy, + openMapPage: widget.openMapPage, + resetRotation: resetRotation, + ); } - -@immutable -class MarkerKey extends LocalKey with EquatableMixin { - final AvesEntry entry; - final int? count; - - @override - List get props => [entry, count]; - - const MarkerKey(this.entry, this.count); -} - -typedef MarkerClusterBuilder = Map Function(); -typedef MarkerWidgetBuilder = Widget Function(MarkerKey key); -typedef UserZoomChangeCallback = void Function(double zoom); -typedef MarkerTapCallback = void Function(LatLng averageLocation, AvesEntry markerEntry, Set Function() getClusterEntries); diff --git a/lib/widgets/common/map/google/geotiff_tile_provider.dart b/lib/widgets/common/map/google/geotiff_tile_provider.dart deleted file mode 100644 index 72d5ccb03..000000000 --- a/lib/widgets/common/map/google/geotiff_tile_provider.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:aves/model/geotiff.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; - -class GeoTiffTileProvider extends TileProvider { - MappedGeoTiff overlayEntry; - - GeoTiffTileProvider(this.overlayEntry); - - @override - Future getTile(int x, int y, int? zoom) async { - final tile = await overlayEntry.getTile(x, y, zoom); - if (tile != null) { - return Tile(tile.width, tile.height, tile.data); - } - return TileProvider.noTile; - } -} diff --git a/lib/widgets/common/map/latlng_tween.dart b/lib/widgets/common/map/leaflet/latlng_tween.dart similarity index 83% rename from lib/widgets/common/map/latlng_tween.dart rename to lib/widgets/common/map/leaflet/latlng_tween.dart index 912e14179..2b3d6960c 100644 --- a/lib/widgets/common/map/latlng_tween.dart +++ b/lib/widgets/common/map/leaflet/latlng_tween.dart @@ -1,4 +1,4 @@ -import 'package:aves/widgets/common/map/latlng_utils.dart'; +import 'package:aves/widgets/common/map/leaflet/latlng_utils.dart'; import 'package:flutter/widgets.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/widgets/common/map/latlng_utils.dart b/lib/widgets/common/map/leaflet/latlng_utils.dart similarity index 100% rename from lib/widgets/common/map/latlng_utils.dart rename to lib/widgets/common/map/leaflet/latlng_utils.dart diff --git a/lib/widgets/common/map/leaflet/map.dart b/lib/widgets/common/map/leaflet/map.dart index 6618cb87f..43931d71b 100644 --- a/lib/widgets/common/map/leaflet/map.dart +++ b/lib/widgets/common/map/leaflet/map.dart @@ -1,52 +1,45 @@ import 'dart:async'; -import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/geotiff.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/debouncer.dart'; -import 'package:aves/widgets/common/map/buttons.dart'; -import 'package:aves/widgets/common/map/controller.dart'; -import 'package:aves/widgets/common/map/decorator.dart'; -import 'package:aves/widgets/common/map/geo_entry.dart'; -import 'package:aves/widgets/common/map/geo_map.dart'; -import 'package:aves/widgets/common/map/latlng_tween.dart'; +import 'package:aves/widgets/common/map/leaflet/latlng_tween.dart'; import 'package:aves/widgets/common/map/leaflet/scale_layer.dart'; import 'package:aves/widgets/common/map/leaflet/tile_layers.dart'; -import 'package:aves/widgets/common/map/marker.dart'; -import 'package:aves/widgets/common/map/theme.dart'; -import 'package:aves/widgets/common/map/zoomed_bounds.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; -class EntryLeafletMap extends StatefulWidget { +class EntryLeafletMap extends StatefulWidget { final AvesMapController? controller; final Listenable clusterListenable; final ValueNotifier boundsNotifier; final double minZoom, maxZoom; final EntryMapStyle style; - final MarkerClusterBuilder markerClusterBuilder; - final MarkerWidgetBuilder markerWidgetBuilder; + final TransitionBuilder decoratorBuilder; + final ButtonPanelBuilder buttonPanelBuilder; + final MarkerClusterBuilder markerClusterBuilder; + final MarkerWidgetBuilder markerWidgetBuilder; final ValueNotifier? dotLocationNotifier; final Size markerSize, dotMarkerSize; final ValueNotifier? overlayOpacityNotifier; - final MappedGeoTiff? overlayEntry; + final MapOverlay? overlayEntry; final UserZoomChangeCallback? onUserZoomChange; - final void Function(LatLng location)? onMapTap; - final void Function(GeoEntry geoEntry)? onMarkerTap; - final MapOpener? openMapPage; + final MapTapCallback? onMapTap; + final MarkerTapCallback? onMarkerTap; const EntryLeafletMap({ - Key? key, + super.key, this.controller, required this.clusterListenable, required this.boundsNotifier, this.minZoom = 0, this.maxZoom = 22, required this.style, + required this.decoratorBuilder, + required this.buttonPanelBuilder, required this.markerClusterBuilder, required this.markerWidgetBuilder, required this.dotLocationNotifier, @@ -57,17 +50,16 @@ class EntryLeafletMap extends StatefulWidget { this.onUserZoomChange, this.onMapTap, this.onMarkerTap, - this.openMapPage, - }) : super(key: key); + }); @override - State createState() => _EntryLeafletMapState(); + State createState() => _EntryLeafletMapState(); } -class _EntryLeafletMapState extends State with TickerProviderStateMixin { +class _EntryLeafletMapState extends State> with TickerProviderStateMixin { final MapController _leafletMapController = MapController(); final List _subscriptions = []; - Map _geoEntryByMarkerKey = {}; + Map, GeoEntry> _geoEntryByMarkerKey = {}; final Debouncer _debouncer = Debouncer(delay: Durations.mapIdleDebounceDelay); ValueNotifier get boundsNotifier => widget.boundsNotifier; @@ -81,11 +73,11 @@ class _EntryLeafletMapState extends State with TickerProviderSt void initState() { super.initState(); _registerWidget(widget); - WidgetsBinding.instance!.addPostFrameCallback((_) => _updateVisibleRegion()); + WidgetsBinding.instance.addPostFrameCallback((_) => _updateVisibleRegion()); } @override - void didUpdateWidget(covariant EntryLeafletMap oldWidget) { + void didUpdateWidget(covariant EntryLeafletMap oldWidget) { super.didUpdateWidget(oldWidget); _unregisterWidget(oldWidget); _registerWidget(widget); @@ -97,7 +89,7 @@ class _EntryLeafletMapState extends State with TickerProviderSt super.dispose(); } - void _registerWidget(EntryLeafletMap widget) { + void _registerWidget(EntryLeafletMap widget) { final avesMapController = widget.controller; if (avesMapController != null) { _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(event.latLng))); @@ -107,7 +99,7 @@ class _EntryLeafletMapState extends State with TickerProviderSt widget.boundsNotifier.addListener(_onBoundsChange); } - void _unregisterWidget(EntryLeafletMap widget) { + void _unregisterWidget(EntryLeafletMap widget) { widget.clusterListenable.removeListener(_updateMarkers); widget.boundsNotifier.removeListener(_onBoundsChange); _subscriptions @@ -119,15 +111,8 @@ class _EntryLeafletMapState extends State with TickerProviderSt Widget build(BuildContext context) { return Stack( children: [ - MapDecorator( - child: _buildMap(), - ), - MapButtonPanel( - boundsNotifier: boundsNotifier, - zoomBy: _zoomBy, - openMapPage: widget.openMapPage, - resetRotation: _resetRotation, - ), + widget.decoratorBuilder(context, _buildMap()), + widget.buttonPanelBuilder(_zoomBy, _resetRotation), ], ); } @@ -239,7 +224,7 @@ class _EntryLeafletMapState extends State with TickerProviderSt overlayImages: [ OverlayImage( bounds: LatLngBounds(corner1, corner2), - imageProvider: overlayEntry.entry.uriImage, + imageProvider: overlayEntry.imageProvider, opacity: overlayOpacity, ), ], diff --git a/lib/widgets/common/map/leaflet/scale_layer.dart b/lib/widgets/common/map/leaflet/scale_layer.dart index 37e76093c..547a1937d 100644 --- a/lib/widgets/common/map/leaflet/scale_layer.dart +++ b/lib/widgets/common/map/leaflet/scale_layer.dart @@ -9,11 +9,11 @@ class ScaleLayerOptions extends LayerOptions { final Widget Function(double width, String distance) builder; ScaleLayerOptions({ - Key? key, + super.key, this.unitSystem = UnitSystem.metric, this.builder = defaultBuilder, rebuild, - }) : super(key: key, rebuild: rebuild); + }) : super(rebuild: rebuild); static Widget defaultBuilder(double width, String distance) { return ScaleBar( @@ -132,10 +132,10 @@ class ScaleBar extends StatelessWidget { static const double barThickness = 1; const ScaleBar({ - Key? key, + super.key, required this.distance, required this.width, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/map/leaflet/tile_layers.dart b/lib/widgets/common/map/leaflet/tile_layers.dart index 069a0967c..5602a58c3 100644 --- a/lib/widgets/common/map/leaflet/tile_layers.dart +++ b/lib/widgets/common/map/leaflet/tile_layers.dart @@ -6,7 +6,7 @@ import 'package:provider/provider.dart'; const _tileLayerBackgroundColor = Colors.transparent; class OSMHotLayer extends StatelessWidget { - const OSMHotLayer({Key? key}) : super(key: key); + const OSMHotLayer({super.key}); @override Widget build(BuildContext context) { @@ -23,7 +23,7 @@ class OSMHotLayer extends StatelessWidget { } class StamenTonerLayer extends StatelessWidget { - const StamenTonerLayer({Key? key}) : super(key: key); + const StamenTonerLayer({super.key}); @override Widget build(BuildContext context) { @@ -40,7 +40,7 @@ class StamenTonerLayer extends StatelessWidget { } class StamenWatercolorLayer extends StatelessWidget { - const StamenWatercolorLayer({Key? key}) : super(key: key); + const StamenWatercolorLayer({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/providers/highlight_info_provider.dart b/lib/widgets/common/providers/highlight_info_provider.dart index 22889f0cc..cdcb7a980 100644 --- a/lib/widgets/common/providers/highlight_info_provider.dart +++ b/lib/widgets/common/providers/highlight_info_provider.dart @@ -6,9 +6,9 @@ class HighlightInfoProvider extends StatelessWidget { final Widget child; const HighlightInfoProvider({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/map/theme.dart b/lib/widgets/common/providers/map_theme_provider.dart similarity index 68% rename from lib/widgets/common/map/theme.dart rename to lib/widgets/common/providers/map_theme_provider.dart index b0e70ee40..96a5d785a 100644 --- a/lib/widgets/common/map/theme.dart +++ b/lib/widgets/common/providers/map_theme_provider.dart @@ -1,9 +1,8 @@ import 'package:aves/model/settings/settings.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -enum MapNavigationButton { back, map } - class MapTheme extends StatelessWidget { final bool interactive, showCoordinateFilter; final MapNavigationButton navigationButton; @@ -13,7 +12,7 @@ class MapTheme extends StatelessWidget { final Widget child; const MapTheme({ - Key? key, + super.key, required this.interactive, required this.showCoordinateFilter, required this.navigationButton, @@ -21,7 +20,7 @@ class MapTheme extends StatelessWidget { this.visualDensity, this.mapHeight, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -40,20 +39,3 @@ class MapTheme extends StatelessWidget { ); } } - -class MapThemeData { - final bool interactive, showCoordinateFilter; - final MapNavigationButton navigationButton; - final Animation scale; - final VisualDensity? visualDensity; - final double? mapHeight; - - const MapThemeData({ - required this.interactive, - required this.showCoordinateFilter, - required this.navigationButton, - required this.scale, - required this.visualDensity, - required this.mapHeight, - }); -} diff --git a/lib/widgets/common/providers/media_query_data_provider.dart b/lib/widgets/common/providers/media_query_data_provider.dart index 302b6f8f4..71c45c4a4 100644 --- a/lib/widgets/common/providers/media_query_data_provider.dart +++ b/lib/widgets/common/providers/media_query_data_provider.dart @@ -5,9 +5,9 @@ class MediaQueryDataProvider extends StatelessWidget { final Widget child; const MediaQueryDataProvider({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/providers/query_provider.dart b/lib/widgets/common/providers/query_provider.dart index 472d444e8..426cb7c56 100644 --- a/lib/widgets/common/providers/query_provider.dart +++ b/lib/widgets/common/providers/query_provider.dart @@ -7,10 +7,10 @@ class QueryProvider extends StatelessWidget { final Widget child; const QueryProvider({ - Key? key, + super.key, required this.initialQuery, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/providers/selection_provider.dart b/lib/widgets/common/providers/selection_provider.dart index 5d44029cf..b1bd9b61a 100644 --- a/lib/widgets/common/providers/selection_provider.dart +++ b/lib/widgets/common/providers/selection_provider.dart @@ -6,9 +6,9 @@ class SelectionProvider extends StatelessWidget { final Widget child; const SelectionProvider({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/providers/tile_extent_controller_provider.dart b/lib/widgets/common/providers/tile_extent_controller_provider.dart index 995279d68..c2e53f309 100644 --- a/lib/widgets/common/providers/tile_extent_controller_provider.dart +++ b/lib/widgets/common/providers/tile_extent_controller_provider.dart @@ -7,10 +7,10 @@ class TileExtentControllerProvider extends StatelessWidget { final Widget child; const TileExtentControllerProvider({ - Key? key, + super.key, required this.controller, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/search/delegate.dart b/lib/widgets/common/search/delegate.dart new file mode 100644 index 000000000..066efcb3b --- /dev/null +++ b/lib/widgets/common/search/delegate.dart @@ -0,0 +1,105 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/search/route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +abstract class AvesSearchDelegate extends SearchDelegate { + final String routeName; + final bool canPop; + + AvesSearchDelegate({ + required this.routeName, + this.canPop = true, + String? initialQuery, + required super.searchFieldLabel, + }) { + query = initialQuery ?? ''; + } + + @override + 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 + return canPop + ? IconButton( + icon: AnimatedIcon( + icon: AnimatedIcons.menu_arrow, + progress: transitionAnimation, + ), + onPressed: () => goBack(context), + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + ) + : const CloseButton( + onPressed: SystemNavigator.pop, + ); + } + + @override + List buildActions(BuildContext context) { + return [ + if (query.isNotEmpty) + IconButton( + icon: const Icon(AIcons.clear), + onPressed: () { + query = ''; + showSuggestions(context); + }, + tooltip: context.l10n.clearTooltip, + ), + ]; + } + + void goBack(BuildContext context) { + clean(); + Navigator.pop(context); + } + + void clean() { + currentBody = null; + focusNode?.unfocus(); + } + + // adapted from Flutter `SearchDelegate` in `/material/search.dart` + + @override + void showResults(BuildContext context) { + focusNode?.unfocus(); + currentBody = SearchBody.results; + } + + @override + void showSuggestions(BuildContext context) { + assert(focusNode != null, '_focusNode must be set by route before showSuggestions is called.'); + focusNode!.requestFocus(); + currentBody = SearchBody.suggestions; + } + + @override + Animation get transitionAnimation => proxyAnimation; + + FocusNode? focusNode; + + final TextEditingController queryTextController = TextEditingController(); + + final ProxyAnimation proxyAnimation = ProxyAnimation(kAlwaysDismissedAnimation); + + @override + String get query => queryTextController.text; + + @override + set query(String value) { + queryTextController.text = value; + } + + final ValueNotifier currentBodyNotifier = ValueNotifier(null); + + SearchBody? get currentBody => currentBodyNotifier.value; + + set currentBody(SearchBody? value) { + currentBodyNotifier.value = value; + } + + SearchPageRoute? route; +} diff --git a/lib/widgets/search/search_page.dart b/lib/widgets/common/search/page.dart similarity index 64% rename from lib/widgets/search/search_page.dart rename to lib/widgets/common/search/page.dart index 801e132a4..0ebe1c5e2 100644 --- a/lib/widgets/search/search_page.dart +++ b/lib/widgets/common/search/page.dart @@ -2,21 +2,21 @@ import 'dart:ui'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/debouncer.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/search/search_delegate.dart'; +import 'package:aves/widgets/common/identity/aves_app_bar.dart'; +import 'package:aves/widgets/common/search/delegate.dart'; +import 'package:aves/widgets/common/search/route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; class SearchPage extends StatefulWidget { - static const routeName = '/search'; - - final CollectionSearchDelegate delegate; + final AvesSearchDelegate delegate; final Animation animation; const SearchPage({ - Key? key, + super.key, required this.delegate, required this.animation, - }) : super(key: key); + }); @override State createState() => _SearchPageState(); @@ -29,42 +29,49 @@ class _SearchPageState extends State { @override void initState() { super.initState(); - widget.delegate.queryTextController.addListener(_onQueryChanged); + _registerWidget(widget); widget.animation.addStatusListener(_onAnimationStatusChanged); - widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged); _focusNode.addListener(_onFocusChanged); - widget.delegate.focusNode = _focusNode; + } + + @override + void didUpdateWidget(covariant SearchPage oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.delegate != oldWidget.delegate) { + _unregisterWidget(oldWidget); + _registerWidget(widget); + } } @override void dispose() { - widget.delegate.queryTextController.removeListener(_onQueryChanged); + _unregisterWidget(widget); widget.animation.removeStatusListener(_onAnimationStatusChanged); - widget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged); - widget.delegate.focusNode = null; _focusNode.dispose(); super.dispose(); } + void _registerWidget(SearchPage widget) { + widget.delegate.queryTextController.addListener(_onQueryChanged); + widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged); + widget.delegate.focusNode = _focusNode; + } + + void _unregisterWidget(SearchPage widget) { + widget.delegate.queryTextController.removeListener(_onQueryChanged); + widget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged); + widget.delegate.focusNode = null; + } + void _onAnimationStatusChanged(AnimationStatus status) { if (status != AnimationStatus.completed) { return; } widget.animation.removeStatusListener(_onAnimationStatusChanged); - _focusNode.requestFocus(); - } - - @override - void didUpdateWidget(covariant SearchPage oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.delegate != oldWidget.delegate) { - oldWidget.delegate.queryTextController.removeListener(_onQueryChanged); - widget.delegate.queryTextController.addListener(_onQueryChanged); - oldWidget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged); - widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged); - oldWidget.delegate.focusNode = null; - widget.delegate.focusNode = _focusNode; - } + Future.delayed(Durations.pageTransitionAnimation * timeDilation).then((_) { + if (!mounted) return; + _focusNode.requestFocus(); + }); } void _onFocusChanged() { @@ -110,19 +117,27 @@ class _SearchPageState extends State { } return Scaffold( appBar: AppBar( - leading: widget.delegate.buildLeading(context), - title: DefaultTextStyle.merge( - style: const TextStyle(fontFeatures: [FontFeature.disable('smcp')]), - child: TextField( - controller: widget.delegate.queryTextController, - focusNode: _focusNode, - style: theme.textTheme.headline6, - textInputAction: TextInputAction.search, - onSubmitted: (_) => widget.delegate.showResults(context), - decoration: InputDecoration( - border: InputBorder.none, - hintText: context.l10n.searchCollectionFieldHint, - hintStyle: theme.inputDecorationTheme.hintStyle, + leading: Hero( + tag: AvesAppBar.leadingHeroTag, + transitionOnUserGestures: true, + child: Center(child: widget.delegate.buildLeading(context)), + ), + title: Hero( + tag: AvesAppBar.titleHeroTag, + transitionOnUserGestures: true, + child: DefaultTextStyle.merge( + style: const TextStyle(fontFeatures: [FontFeature.disable('smcp')]), + child: TextField( + controller: widget.delegate.queryTextController, + focusNode: _focusNode, + decoration: InputDecoration( + border: InputBorder.none, + hintText: widget.delegate.searchFieldLabel, + hintStyle: theme.inputDecorationTheme.hintStyle, + ), + textInputAction: TextInputAction.search, + style: theme.textTheme.headline6, + onSubmitted: (_) => widget.delegate.showResults(context), ), ), ), diff --git a/lib/widgets/common/search/route.dart b/lib/widgets/common/search/route.dart new file mode 100644 index 000000000..c2904d8ba --- /dev/null +++ b/lib/widgets/common/search/route.dart @@ -0,0 +1,75 @@ +import 'package:aves/widgets/common/search/delegate.dart'; +import 'package:aves/widgets/common/search/page.dart'; +import 'package:flutter/material.dart'; + +// adapted from Flutter `_SearchBody` in `/material/search.dart` +enum SearchBody { suggestions, results } + +// adapted from Flutter `_SearchPageRoute` in `/material/search.dart` +class SearchPageRoute extends PageRoute { + SearchPageRoute({ + required this.delegate, + }) : super(settings: RouteSettings(name: delegate.routeName)) { + assert( + 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.', + ); + delegate.route = this; + } + + final AvesSearchDelegate delegate; + + @override + Color? get barrierColor => null; + + @override + String? get barrierLabel => null; + + @override + Duration get transitionDuration => const Duration(milliseconds: 300); + + @override + bool get maintainState => false; + + @override + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return FadeTransition( + opacity: animation, + child: child, + ); + } + + @override + Animation createAnimation() { + final animation = super.createAnimation(); + delegate.proxyAnimation.parent = animation; + return animation; + } + + @override + Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return SearchPage( + delegate: delegate, + animation: animation, + ); + } + + @override + void didComplete(T? result) { + super.didComplete(result); + assert(delegate.route == this); + delegate.route = null; + delegate.currentBody = null; + } +} diff --git a/lib/widgets/common/sliver_app_bar_title.dart b/lib/widgets/common/sliver_app_bar_title.dart index 5d4f786eb..3c04c231f 100644 --- a/lib/widgets/common/sliver_app_bar_title.dart +++ b/lib/widgets/common/sliver_app_bar_title.dart @@ -7,9 +7,9 @@ class SliverAppBarTitleWrapper extends StatelessWidget { final Widget child; const SliverAppBarTitleWrapper({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/thumbnail/decorated.dart b/lib/widgets/common/thumbnail/decorated.dart index 5c0b65f39..336a3f8c9 100644 --- a/lib/widgets/common/thumbnail/decorated.dart +++ b/lib/widgets/common/thumbnail/decorated.dart @@ -16,14 +16,14 @@ class DecoratedThumbnail extends StatelessWidget { static final double borderWidth = AvesBorder.straightBorderWidth; const DecoratedThumbnail({ - Key? key, + super.key, required this.entry, required this.tileExtent, this.cancellableNotifier, this.selectable = true, this.highlightable = true, this.heroTagger, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/thumbnail/error.dart b/lib/widgets/common/thumbnail/error.dart index b00266454..bb6fa6000 100644 --- a/lib/widgets/common/thumbnail/error.dart +++ b/lib/widgets/common/thumbnail/error.dart @@ -12,10 +12,10 @@ class ErrorThumbnail extends StatefulWidget { final double extent; const ErrorThumbnail({ - Key? key, + super.key, required this.entry, required this.extent, - }) : super(key: key); + }); @override State createState() => _ErrorThumbnailState(); diff --git a/lib/widgets/common/thumbnail/image.dart b/lib/widgets/common/thumbnail/image.dart index 558ae4153..ae4542f72 100644 --- a/lib/widgets/common/thumbnail/image.dart +++ b/lib/widgets/common/thumbnail/image.dart @@ -26,7 +26,7 @@ class ThumbnailImage extends StatefulWidget { final Object? heroTag; const ThumbnailImage({ - Key? key, + super.key, required this.entry, required this.extent, this.progressive = true, @@ -34,7 +34,7 @@ class ThumbnailImage extends StatefulWidget { this.showLoadingBackground = true, this.cancellableNotifier, this.heroTag, - }) : super(key: key); + }); @override State createState() => _ThumbnailImageState(); diff --git a/lib/widgets/common/thumbnail/overlay.dart b/lib/widgets/common/thumbnail/overlay.dart index d3859d1bd..393af4178 100644 --- a/lib/widgets/common/thumbnail/overlay.dart +++ b/lib/widgets/common/thumbnail/overlay.dart @@ -12,14 +12,15 @@ class ThumbnailEntryOverlay extends StatelessWidget { final AvesEntry entry; const ThumbnailEntryOverlay({ - Key? key, + super.key, required this.entry, - }) : super(key: key); + }); @override Widget build(BuildContext context) { final children = [ if (entry.isFavourite && context.select((t) => t.showFavourite)) const FavouriteIcon(), + if (entry.tags.isNotEmpty && context.select((t) => t.showTag)) const TagIcon(), if (entry.hasGps && context.select((t) => t.showLocation)) const GpsIcon(), if (entry.rating != 0 && context.select((t) => t.showRating)) RatingIcon(entry: entry), if (entry.isVideo) @@ -53,9 +54,9 @@ class ThumbnailHighlightOverlay extends StatefulWidget { final AvesEntry entry; const ThumbnailHighlightOverlay({ - Key? key, + super.key, required this.entry, - }) : super(key: key); + }); @override State createState() => _ThumbnailHighlightOverlayState(); diff --git a/lib/widgets/common/thumbnail/scroller.dart b/lib/widgets/common/thumbnail/scroller.dart index a74e32bf5..4c8162149 100644 --- a/lib/widgets/common/thumbnail/scroller.dart +++ b/lib/widgets/common/thumbnail/scroller.dart @@ -18,7 +18,7 @@ class ThumbnailScroller extends StatefulWidget { final bool scrollable, highlightable, showLocation; const ThumbnailScroller({ - Key? key, + super.key, required this.availableWidth, required this.entryCount, required this.entryBuilder, @@ -28,7 +28,7 @@ class ThumbnailScroller extends StatefulWidget { this.highlightable = false, this.showLocation = true, this.scrollable = true, - }) : super(key: key); + }); @override State createState() => _ThumbnailScrollerState(); diff --git a/lib/widgets/debug/android_apps.dart b/lib/widgets/debug/android_apps.dart index b82710219..af96119e2 100644 --- a/lib/widgets/debug/android_apps.dart +++ b/lib/widgets/debug/android_apps.dart @@ -8,7 +8,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; class DebugAndroidAppSection extends StatefulWidget { - const DebugAndroidAppSection({Key? key}) : super(key: key); + const DebugAndroidAppSection({super.key}); @override State createState() => _DebugAndroidAppSectionState(); diff --git a/lib/widgets/debug/android_codecs.dart b/lib/widgets/debug/android_codecs.dart index dc2c47c12..bba35f549 100644 --- a/lib/widgets/debug/android_codecs.dart +++ b/lib/widgets/debug/android_codecs.dart @@ -7,7 +7,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; class DebugAndroidCodecSection extends StatefulWidget { - const DebugAndroidCodecSection({Key? key}) : super(key: key); + const DebugAndroidCodecSection({super.key}); @override State createState() => _DebugAndroidCodecSectionState(); diff --git a/lib/widgets/debug/android_dirs.dart b/lib/widgets/debug/android_dirs.dart index 5b5bd1a36..fd92c851e 100644 --- a/lib/widgets/debug/android_dirs.dart +++ b/lib/widgets/debug/android_dirs.dart @@ -6,7 +6,7 @@ import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; class DebugAndroidDirSection extends StatefulWidget { - const DebugAndroidDirSection({Key? key}) : super(key: key); + const DebugAndroidDirSection({super.key}); @override State createState() => _DebugAndroidDirSectionState(); diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 7223094ec..12751b2d3 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/path.dart'; import 'package:aves/model/filters/tag.dart'; @@ -31,17 +30,13 @@ import 'package:provider/provider.dart'; class AppDebugPage extends StatefulWidget { static const routeName = '/debug'; - const AppDebugPage({Key? key}) : super(key: key); + const AppDebugPage({super.key}); @override State createState() => _AppDebugPageState(); } class _AppDebugPageState extends State { - CollectionSource get source => context.read(); - - Set get visibleEntries => source.visibleEntries; - static OverlayEntry? _taskQueueOverlayEntry; @override @@ -96,6 +91,8 @@ class _AppDebugPageState extends State { } Widget _buildGeneralTabView() { + final source = context.read(); + final visibleEntries = source.visibleEntries; final catalogued = visibleEntries.where((entry) => entry.isCatalogued); final withGps = catalogued.where((entry) => entry.hasGps); final withAddress = withGps.where((entry) => entry.hasAddress); @@ -172,6 +169,8 @@ class _AppDebugPageState extends State { Future _onActionSelected(AppDebugAction action) async { switch (action) { case AppDebugAction.prepScreenshotThumbnails: + // get source beforehand, as widget may be unmounted during action handling + final source = context.read(); settings.changeFilterVisibility(settings.hiddenFilters, true); settings.changeFilterVisibility({ TagFilter('aves-thumbnail', not: true), diff --git a/lib/widgets/debug/cache.dart b/lib/widgets/debug/cache.dart index 28bbcc23a..1ba8e936c 100644 --- a/lib/widgets/debug/cache.dart +++ b/lib/widgets/debug/cache.dart @@ -4,7 +4,7 @@ import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:flutter/material.dart'; class DebugCacheSection extends StatefulWidget { - const DebugCacheSection({Key? key}) : super(key: key); + const DebugCacheSection({super.key}); @override State createState() => _DebugCacheSectionState(); @@ -25,12 +25,12 @@ class _DebugCacheSectionState extends State with AutomaticKee Row( children: [ Expanded( - child: Text('Image cache:\n\t${imageCache!.currentSize}/${imageCache!.maximumSize} items\n\t${formatFileSize('en_US', imageCache!.currentSizeBytes)}/${formatFileSize('en_US', imageCache!.maximumSizeBytes)}'), + child: Text('Image cache:\n\t${imageCache.currentSize}/${imageCache.maximumSize} items\n\t${formatFileSize('en_US', imageCache.currentSizeBytes)}/${formatFileSize('en_US', imageCache.maximumSizeBytes)}'), ), const SizedBox(width: 8), ElevatedButton( onPressed: () { - imageCache!.clear(); + imageCache.clear(); setState(() {}); }, diff --git a/lib/widgets/debug/database.dart b/lib/widgets/debug/database.dart index 60eaad3b6..596eacdcd 100644 --- a/lib/widgets/debug/database.dart +++ b/lib/widgets/debug/database.dart @@ -11,7 +11,7 @@ import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:flutter/material.dart'; class DebugAppDatabaseSection extends StatefulWidget { - const DebugAppDatabaseSection({Key? key}) : super(key: key); + const DebugAppDatabaseSection({super.key}); @override State createState() => _DebugAppDatabaseSectionState(); diff --git a/lib/widgets/debug/media_store_scan_dialog.dart b/lib/widgets/debug/media_store_scan_dialog.dart index a63d07202..f3c150bdc 100644 --- a/lib/widgets/debug/media_store_scan_dialog.dart +++ b/lib/widgets/debug/media_store_scan_dialog.dart @@ -6,7 +6,7 @@ import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:flutter/material.dart'; class MediaStoreScanDirDialog extends StatefulWidget { - const MediaStoreScanDirDialog({Key? key}) : super(key: key); + const MediaStoreScanDirDialog({super.key}); @override State createState() => _MediaStoreScanDirDialogState(); diff --git a/lib/widgets/debug/overlay.dart b/lib/widgets/debug/overlay.dart index 143a798bd..b2984037b 100644 --- a/lib/widgets/debug/overlay.dart +++ b/lib/widgets/debug/overlay.dart @@ -2,7 +2,7 @@ import 'package:aves/services/common/service_policy.dart'; import 'package:flutter/material.dart'; class DebugTaskQueueOverlay extends StatelessWidget { - const DebugTaskQueueOverlay({Key? key}) : super(key: key); + const DebugTaskQueueOverlay({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/debug/report.dart b/lib/widgets/debug/report.dart index b97df4b14..c59676c94 100644 --- a/lib/widgets/debug/report.dart +++ b/lib/widgets/debug/report.dart @@ -5,7 +5,7 @@ import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; class DebugErrorReportingSection extends StatelessWidget { - const DebugErrorReportingSection({Key? key}) : super(key: key); + const DebugErrorReportingSection({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart index cb2c286a3..f84b1c897 100644 --- a/lib/widgets/debug/settings.dart +++ b/lib/widgets/debug/settings.dart @@ -9,7 +9,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class DebugSettingsSection extends StatelessWidget { - const DebugSettingsSection({Key? key}) : super(key: key); + const DebugSettingsSection({super.key}); @override Widget build(BuildContext context) { @@ -66,7 +66,7 @@ class DebugSettingsSection extends StatelessWidget { 'hiddenFilters': toMultiline(settings.hiddenFilters), 'searchHistory': toMultiline(settings.searchHistory), 'locale': '${settings.locale}', - 'systemLocales': '${WidgetsBinding.instance!.window.locales}', + 'systemLocales': '${WidgetsBinding.instance.window.locales}', 'topEntryIds': '${settings.topEntryIds}', }, ), diff --git a/lib/widgets/debug/storage.dart b/lib/widgets/debug/storage.dart index e7c697590..8ea4ba4dc 100644 --- a/lib/widgets/debug/storage.dart +++ b/lib/widgets/debug/storage.dart @@ -6,7 +6,7 @@ import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; class DebugStorageSection extends StatefulWidget { - const DebugStorageSection({Key? key}) : super(key: key); + const DebugStorageSection({super.key}); @override State createState() => _DebugStorageSectionState(); diff --git a/lib/widgets/dialogs/add_shortcut_dialog.dart b/lib/widgets/dialogs/add_shortcut_dialog.dart index b7e8c9cff..f78cda820 100644 --- a/lib/widgets/dialogs/add_shortcut_dialog.dart +++ b/lib/widgets/dialogs/add_shortcut_dialog.dart @@ -17,10 +17,10 @@ class AddShortcutDialog extends StatefulWidget { final String defaultName; const AddShortcutDialog({ - Key? key, + super.key, required this.defaultName, this.collection, - }) : super(key: key); + }); @override State createState() => _AddShortcutDialogState(); diff --git a/lib/widgets/dialogs/app_pick_dialog.dart b/lib/widgets/dialogs/app_pick_dialog.dart index 8819fedd9..13a0e22cc 100644 --- a/lib/widgets/dialogs/app_pick_dialog.dart +++ b/lib/widgets/dialogs/app_pick_dialog.dart @@ -14,9 +14,9 @@ class AppPickDialog extends StatefulWidget { final String? initialValue; const AppPickDialog({ - Key? key, + super.key, required this.initialValue, - }) : super(key: key); + }); @override State createState() => _AppPickDialogState(); diff --git a/lib/widgets/dialogs/aves_confirmation_dialog.dart b/lib/widgets/dialogs/aves_confirmation_dialog.dart index 36f77c408..8fcedc0b6 100644 --- a/lib/widgets/dialogs/aves_confirmation_dialog.dart +++ b/lib/widgets/dialogs/aves_confirmation_dialog.dart @@ -83,11 +83,10 @@ class _AvesConfirmationDialog extends StatefulWidget { final String confirmationButtonLabel; const _AvesConfirmationDialog({ - Key? key, required this.type, required this.delegate, required this.confirmationButtonLabel, - }) : super(key: key); + }); @override State<_AvesConfirmationDialog> createState() => _AvesConfirmationDialogState(); diff --git a/lib/widgets/dialogs/aves_dialog.dart b/lib/widgets/dialogs/aves_dialog.dart index cdc9d6d2a..0421e4fc5 100644 --- a/lib/widgets/dialogs/aves_dialog.dart +++ b/lib/widgets/dialogs/aves_dialog.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; class AvesDialog extends StatelessWidget { final String? title; - final ScrollController? scrollController; + final ScrollController scrollController; final List? scrollableContent; final bool hasScrollBar; final double horizontalContentPadding; @@ -16,17 +16,17 @@ class AvesDialog extends StatelessWidget { static const double controlCaptionPadding = 16; static const double borderWidth = 1.0; - const AvesDialog({ - Key? key, + AvesDialog({ + super.key, this.title, - this.scrollController, + ScrollController? scrollController, this.scrollableContent, this.hasScrollBar = true, this.horizontalContentPadding = defaultHorizontalContentPadding, this.content, required this.actions, }) : assert((scrollableContent != null) ^ (content != null)), - super(key: key); + scrollController = scrollController ?? ScrollController(); @override Widget build(BuildContext context) { @@ -57,10 +57,8 @@ class AvesDialog extends StatelessWidget { } if (scrollableContent != null) { - final _scrollController = scrollController ?? ScrollController(); - Widget child = ListView( - controller: _scrollController, + controller: scrollController, shrinkWrap: true, children: scrollableContent!, ); @@ -68,16 +66,23 @@ class AvesDialog extends StatelessWidget { if (hasScrollBar) { child = Theme( data: Theme.of(context).copyWith( - scrollbarTheme: const ScrollbarThemeData( - isAlwaysShown: true, - radius: Radius.circular(16), + scrollbarTheme: ScrollbarThemeData( + thumbVisibility: MaterialStateProperty.all(true), + radius: const Radius.circular(16), crossAxisMargin: 4, mainAxisMargin: 4, interactive: true, ), ), child: Scrollbar( - controller: _scrollController, + controller: scrollController, + notificationPredicate: (notification) { + // as of Flutter v3.0.1, the `Scrollbar` does not only respond to the nearest `ScrollView` + // despite the `defaultScrollNotificationPredicate` checking notification depth, + // as the notifications coming from the controller in `ListWheelScrollView` in `WheelSelector` still have a depth of 0. + // Cancelling notification bubbling seems ineffective, so we check the metrics type as a workaround. + return defaultScrollNotificationPredicate(notification) && notification.metrics is! FixedExtentMetrics; + }, child: child, ), ); @@ -120,9 +125,9 @@ class DialogTitle extends StatelessWidget { final String title; const DialogTitle({ - Key? key, + super.key, required this.title, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/dialogs/aves_selection_dialog.dart b/lib/widgets/dialogs/aves_selection_dialog.dart index 63bb05c34..a06c385f6 100644 --- a/lib/widgets/dialogs/aves_selection_dialog.dart +++ b/lib/widgets/dialogs/aves_selection_dialog.dart @@ -31,7 +31,7 @@ class AvesSelectionDialog extends StatefulWidget { final bool? dense; const AvesSelectionDialog({ - Key? key, + super.key, required this.initialValue, required this.options, this.optionSubtitleBuilder, @@ -39,7 +39,7 @@ class AvesSelectionDialog extends StatefulWidget { this.message, this.confirmationButtonLabel, this.dense, - }) : super(key: key); + }); @override State> createState() => _AvesSelectionDialogState(); @@ -92,7 +92,7 @@ class _AvesSelectionDialogState extends State> { groupValue: _selectedValue, onChanged: (v) { if (needConfirmation) { - setState(() => _selectedValue = v!); + setState(() => _selectedValue = v as T); } else { Navigator.pop(context, v); } diff --git a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart index 25b00d606..749d0054c 100644 --- a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart @@ -21,10 +21,10 @@ class EditEntryDateDialog extends StatefulWidget { final CollectionLens? collection; const EditEntryDateDialog({ - Key? key, + super.key, required this.entry, this.collection, - }) : super(key: key); + }); @override State createState() => _EditEntryDateDialogState(); diff --git a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart index 20520155e..649052d67 100644 --- a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart @@ -15,10 +15,10 @@ class EditEntryLocationDialog extends StatefulWidget { final CollectionLens? collection; const EditEntryLocationDialog({ - Key? key, + super.key, required this.entry, this.collection, - }) : super(key: key); + }); @override State createState() => _EditEntryLocationDialogState(); @@ -37,7 +37,7 @@ class _EditEntryLocationDialogState extends State { super.initState(); _latitudeFocusNode.addListener(_onLatLngFocusChange); _longitudeFocusNode.addListener(_onLatLngFocusChange); - WidgetsBinding.instance!.addPostFrameCallback((_) => _setLocation(context, widget.entry.latLng)); + WidgetsBinding.instance.addPostFrameCallback((_) => _setLocation(context, widget.entry.latLng)); } @override diff --git a/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart index 275aab264..49c848423 100644 --- a/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart @@ -9,9 +9,9 @@ class EditEntryRatingDialog extends StatefulWidget { final AvesEntry entry; const EditEntryRatingDialog({ - Key? key, + super.key, required this.entry, - }) : super(key: key); + }); @override State createState() => _EditEntryRatingDialogState(); diff --git a/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart index 09ee22b97..d243e54b1 100644 --- a/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_tags_dialog.dart @@ -19,9 +19,9 @@ class TagEditorPage extends StatefulWidget { final Map> tagsByEntry; const TagEditorPage({ - Key? key, + super.key, required this.tagsByEntry, - }) : super(key: key); + }); @override State createState() => _TagEditorPageState(); @@ -229,12 +229,11 @@ class _FilterRow extends StatelessWidget { final void Function(String tag) onTap; const _FilterRow({ - Key? key, required this.title, required this.filters, required this.expandedNotifier, required this.onTap, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -255,9 +254,8 @@ class _TagCount extends StatelessWidget { final int count; const _TagCount({ - Key? key, required this.count, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart b/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart index ec9c74360..1cb08eec6 100644 --- a/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart @@ -17,9 +17,9 @@ class RemoveEntryMetadataDialog extends StatefulWidget { final bool showJpegTypes; const RemoveEntryMetadataDialog({ - Key? key, + super.key, required this.showJpegTypes, - }) : super(key: key); + }); @override State createState() => _RemoveEntryMetadataDialogState(); diff --git a/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart b/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart index b3c5875a2..b68aa54d2 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart @@ -12,9 +12,9 @@ class RenameEntryDialog extends StatefulWidget { final AvesEntry entry; const RenameEntryDialog({ - Key? key, + super.key, required this.entry, - }) : super(key: key); + }); @override State createState() => _RenameEntryDialogState(); 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 fd749797b..df9beaa39 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_set_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_set_dialog.dart @@ -23,9 +23,9 @@ class RenameEntrySetPage extends StatefulWidget { final List entries; const RenameEntrySetPage({ - Key? key, + super.key, required this.entries, - }) : super(key: key); + }); @override State createState() => _RenameEntrySetPageState(); diff --git a/lib/widgets/dialogs/export_entry_dialog.dart b/lib/widgets/dialogs/export_entry_dialog.dart index 8f5ded8e9..6ef6d5787 100644 --- a/lib/widgets/dialogs/export_entry_dialog.dart +++ b/lib/widgets/dialogs/export_entry_dialog.dart @@ -11,9 +11,9 @@ class ExportEntryDialog extends StatefulWidget { final AvesEntry entry; const ExportEntryDialog({ - Key? key, + super.key, required this.entry, - }) : super(key: key); + }); @override State createState() => _ExportEntryDialogState(); diff --git a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart index 057eb3346..c46f34f3d 100644 --- a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart @@ -28,12 +28,12 @@ class CoverSelectionDialog extends StatefulWidget { final Color? customColor; const CoverSelectionDialog({ - Key? key, + super.key, required this.filter, required this.customEntry, required this.customPackage, required this.customColor, - }) : super(key: key); + }); @override State createState() => _CoverSelectionDialogState(); diff --git a/lib/widgets/dialogs/filter_editors/create_album_dialog.dart b/lib/widgets/dialogs/filter_editors/create_album_dialog.dart index ba5ffc5f1..89312f415 100644 --- a/lib/widgets/dialogs/filter_editors/create_album_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/create_album_dialog.dart @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; import '../aves_dialog.dart'; class CreateAlbumDialog extends StatefulWidget { - const CreateAlbumDialog({Key? key}) : super(key: key); + const CreateAlbumDialog({super.key}); @override State createState() => _CreateAlbumDialogState(); diff --git a/lib/widgets/dialogs/filter_editors/rename_album_dialog.dart b/lib/widgets/dialogs/filter_editors/rename_album_dialog.dart index 525fb8ff4..f270af325 100644 --- a/lib/widgets/dialogs/filter_editors/rename_album_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/rename_album_dialog.dart @@ -9,9 +9,9 @@ class RenameAlbumDialog extends StatefulWidget { final String album; const RenameAlbumDialog({ - Key? key, + super.key, required this.album, - }) : super(key: key); + }); @override State createState() => _RenameAlbumDialogState(); diff --git a/lib/widgets/dialogs/item_pick_dialog.dart b/lib/widgets/dialogs/item_pick_dialog.dart index 184187a04..29627b8a3 100644 --- a/lib/widgets/dialogs/item_pick_dialog.dart +++ b/lib/widgets/dialogs/item_pick_dialog.dart @@ -18,9 +18,9 @@ class ItemPickDialog extends StatefulWidget { final CollectionLens collection; const ItemPickDialog({ - Key? key, + super.key, required this.collection, - }) : super(key: key); + }); @override State createState() => _ItemPickDialogState(); @@ -47,6 +47,7 @@ class _ItemPickDialogState extends State { initialQuery: liveFilter?.query, child: GestureAreaProtectorStack( child: SafeArea( + top: false, bottom: false, child: ChangeNotifierProvider.value( value: collection, diff --git a/lib/widgets/dialogs/item_picker.dart b/lib/widgets/dialogs/item_picker.dart index b70d7ecc9..2c5c2631d 100644 --- a/lib/widgets/dialogs/item_picker.dart +++ b/lib/widgets/dialogs/item_picker.dart @@ -13,11 +13,11 @@ class ItemPicker extends StatelessWidget { final GestureTapCallback? onTap; const ItemPicker({ - Key? key, + super.key, required this.extent, required this.entry, this.onTap, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/dialogs/location_pick_dialog.dart b/lib/widgets/dialogs/location_pick_dialog.dart index 00b3ede5d..b9b7d1b14 100644 --- a/lib/widgets/dialogs/location_pick_dialog.dart +++ b/lib/widgets/dialogs/location_pick_dialog.dart @@ -12,13 +12,10 @@ import 'package:aves/utils/constants.dart'; import 'package:aves/utils/debouncer.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons.dart'; -import 'package:aves/widgets/common/map/controller.dart'; import 'package:aves/widgets/common/map/geo_map.dart'; -import 'package:aves/widgets/common/map/marker.dart'; -import 'package:aves/widgets/common/map/theme.dart'; -import 'package:aves/widgets/common/map/zoomed_bounds.dart'; +import 'package:aves/widgets/common/providers/map_theme_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; -import 'package:decorated_icon/decorated_icon.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:latlong2/latlong.dart'; @@ -31,10 +28,10 @@ class LocationPickDialog extends StatelessWidget { final LatLng? initialLocation; const LocationPickDialog({ - Key? key, + super.key, required this.collection, required this.initialLocation, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -60,10 +57,9 @@ class _Content extends StatefulWidget { final LatLng? initialLocation; const _Content({ - Key? key, required this.collection, required this.initialLocation, - }) : super(key: key); + }); @override State<_Content> createState() => _ContentState(); @@ -82,7 +78,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin void initState() { super.initState(); - if (settings.infoMapStyle.isGoogleMaps) { + if (settings.infoMapStyle.isHeavy) { _isPageAnimatingNotifier = ValueNotifier(true); Future.delayed(Durations.pageTransitionAnimation * timeDilation).then((_) { if (!mounted) return; @@ -180,9 +176,8 @@ class _LocationInfo extends StatelessWidget { static const double _interRowPadding = 2.0; const _LocationInfo({ - Key? key, required this.locationNotifier, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -232,9 +227,8 @@ class _AddressRow extends StatefulWidget { final LatLng? location; const _AddressRow({ - Key? key, required this.location, - }) : super(key: key); + }); @override State<_AddressRow> createState() => _AddressRowState(); @@ -263,7 +257,7 @@ class _AddressRowState extends State<_AddressRow> { mainAxisSize: MainAxisSize.min, children: [ const SizedBox(width: _LocationInfo.iconPadding), - const DecoratedIcon(AIcons.location, size: _LocationInfo.iconSize), + const Icon(AIcons.location, size: _LocationInfo.iconSize), const SizedBox(width: _LocationInfo.iconPadding), Expanded( child: Container( @@ -314,16 +308,15 @@ class _CoordinateRow extends StatelessWidget { final LatLng? location; const _CoordinateRow({ - Key? key, required this.location, - }) : super(key: key); + }); @override Widget build(BuildContext context) { return Row( children: [ const SizedBox(width: _LocationInfo.iconPadding), - const DecoratedIcon(AIcons.geoBounds, size: _LocationInfo.iconSize), + const Icon(AIcons.geoBounds, size: _LocationInfo.iconSize), const SizedBox(width: _LocationInfo.iconPadding), Text( location != null ? settings.coordinateFormat.format(context.l10n, location!) : Constants.overlayUnknown, diff --git a/lib/widgets/dialogs/tile_view_dialog.dart b/lib/widgets/dialogs/tile_view_dialog.dart index 75fd54435..b43715f13 100644 --- a/lib/widgets/dialogs/tile_view_dialog.dart +++ b/lib/widgets/dialogs/tile_view_dialog.dart @@ -16,12 +16,12 @@ class TileViewDialog extends StatefulWidget { final Map layoutOptions; const TileViewDialog({ - Key? key, + super.key, required this.initialValue, this.sortOptions = const {}, this.groupOptions = const {}, this.layoutOptions = const {}, - }) : super(key: key); + }); @override State createState() => _TileViewDialogState(); @@ -232,7 +232,7 @@ class _TileViewDialogState extends State> with key: Key(value.toString()), value: value, groupValue: get(), - onChanged: (v) => setState(() => set(v!)), + onChanged: (v) => setState(() => set(v as T)), title: Text( title, softWrap: false, diff --git a/lib/widgets/dialogs/video_speed_dialog.dart b/lib/widgets/dialogs/video_speed_dialog.dart index a61a53d86..4b44b8cb9 100644 --- a/lib/widgets/dialogs/video_speed_dialog.dart +++ b/lib/widgets/dialogs/video_speed_dialog.dart @@ -7,11 +7,11 @@ class VideoSpeedDialog extends StatefulWidget { final double current, min, max; const VideoSpeedDialog({ - Key? key, + super.key, required this.current, required this.min, required this.max, - }) : super(key: key); + }); @override State createState() => _VideoSpeedDialogState(); diff --git a/lib/widgets/dialogs/video_stream_selection_dialog.dart b/lib/widgets/dialogs/video_stream_selection_dialog.dart index 106f00ae5..87f5e8435 100644 --- a/lib/widgets/dialogs/video_stream_selection_dialog.dart +++ b/lib/widgets/dialogs/video_stream_selection_dialog.dart @@ -13,9 +13,9 @@ class VideoStreamSelectionDialog extends StatefulWidget { final Map streams; const VideoStreamSelectionDialog({ - Key? key, + super.key, required this.streams, - }) : super(key: key); + }); @override State createState() => _VideoStreamSelectionDialogState(); diff --git a/lib/widgets/drawer/tile.dart b/lib/widgets/drawer/tile.dart deleted file mode 100644 index fa59ff537..000000000 --- a/lib/widgets/drawer/tile.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:aves/model/filters/favourite.dart'; -import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/filters/mime.dart'; -import 'package:aves/model/filters/type.dart'; -import 'package:aves/theme/colors.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/debug/app_debug_page.dart'; -import 'package:aves/widgets/filter_grids/albums_page.dart'; -import 'package:aves/widgets/filter_grids/countries_page.dart'; -import 'package:aves/widgets/filter_grids/tags_page.dart'; -import 'package:flutter/material.dart'; - -class DrawerFilterIcon extends StatelessWidget { - final CollectionFilter? filter; - - const DrawerFilterIcon({ - Key? key, - required this.filter, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final textScaleFactor = MediaQuery.textScaleFactorOf(context); - final iconSize = 24 * textScaleFactor; - - final _filter = filter; - if (_filter == null) return Icon(AIcons.allCollection, size: iconSize); - return _filter.iconBuilder(context, iconSize) ?? const SizedBox(); - } -} - -class DrawerFilterTitle extends StatelessWidget { - final CollectionFilter? filter; - - const DrawerFilterTitle({ - Key? key, - required this.filter, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - String _getString(CollectionFilter? filter) { - final l10n = context.l10n; - if (filter == null) return l10n.drawerCollectionAll; - if (filter == FavouriteFilter.instance) return l10n.drawerCollectionFavourites; - if (filter == MimeFilter.image) return l10n.drawerCollectionImages; - if (filter == MimeFilter.video) return l10n.drawerCollectionVideos; - if (filter == TypeFilter.animated) return l10n.drawerCollectionAnimated; - if (filter == TypeFilter.motionPhoto) return l10n.drawerCollectionMotionPhotos; - if (filter == TypeFilter.panorama) return l10n.drawerCollectionPanoramas; - if (filter == TypeFilter.raw) return l10n.drawerCollectionRaws; - if (filter == TypeFilter.sphericalVideo) return l10n.drawerCollectionSphericalVideos; - return filter.getLabel(context); - } - - return Text(_getString(filter)); - } -} - -class DrawerPageIcon extends StatelessWidget { - final String route; - - const DrawerPageIcon({ - Key? key, - required this.route, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - switch (route) { - case AlbumListPage.routeName: - return const Icon(AIcons.album); - case CountryListPage.routeName: - return const Icon(AIcons.location); - case TagListPage.routeName: - return const Icon(AIcons.tag); - case AppDebugPage.routeName: - return ShaderMask( - shaderCallback: AvesColorsData.debugGradient.createShader, - blendMode: BlendMode.srcIn, - child: const Icon(AIcons.debug), - ); - default: - return const SizedBox(); - } - } -} - -class DrawerPageTitle extends StatelessWidget { - final String route; - - const DrawerPageTitle({ - Key? key, - required this.route, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - String _getString() { - final l10n = context.l10n; - switch (route) { - case AlbumListPage.routeName: - return l10n.albumPageTitle; - case CountryListPage.routeName: - return l10n.countryPageTitle; - case TagListPage.routeName: - return l10n.tagPageTitle; - case AppDebugPage.routeName: - return 'Debug'; - default: - return route; - } - } - - return Text(_getString()); - } -} diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index ccdcee07c..928954be5 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -13,10 +13,10 @@ import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/query_bar.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_app_bar.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/selection_provider.dart'; -import 'package:aves/widgets/common/sliver_app_bar_title.dart'; import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/album_set.dart'; @@ -52,10 +52,9 @@ class _AlbumPickPage extends StatefulWidget { final MoveType? moveType; const _AlbumPickPage({ - Key? key, required this.source, required this.moveType, - }) : super(key: key); + }); @override State<_AlbumPickPage> createState() => _AlbumPickPageState(); @@ -86,7 +85,7 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { actionDelegate: AlbumChipSetActionDelegate(gridItems), queryNotifier: _queryNotifier, ), - appBarHeight: _AlbumPickAppBar.preferredHeight, + appBarHeight: AvesAppBar.appBarHeightForContentHeight(_AlbumPickAppBar.contentHeight), sections: AlbumListPage.groupToSections(context, source, gridItems), newFilters: source.getNewAlbumFilters(context), sortFactor: settings.albumSortFactor, @@ -119,15 +118,14 @@ class _AlbumPickAppBar extends StatelessWidget { final AlbumChipSetActionDelegate actionDelegate; final ValueNotifier queryNotifier; - static const preferredHeight = kToolbarHeight + _AlbumQueryBar.preferredHeight; + static const contentHeight = kToolbarHeight + _AlbumQueryBar.preferredHeight; const _AlbumPickAppBar({ - Key? key, required this.source, required this.moveType, required this.actionDelegate, required this.queryNotifier, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -146,59 +144,61 @@ class _AlbumPickAppBar extends StatelessWidget { } } - return SliverAppBar( + return AvesAppBar( + contentHeight: contentHeight, leading: const BackButton(), - title: SliverAppBarTitleWrapper( - child: SourceStateAwareAppBarTitle( - title: Text(title()), - source: source, - ), + title: SourceStateAwareAppBarTitle( + title: Text(title()), + source: source, ), + actions: _buildActions(context), bottom: _AlbumQueryBar( queryNotifier: queryNotifier, ), - actions: [ - if (moveType != null) - IconButton( - icon: const Icon(AIcons.add), - onPressed: () async { - final newAlbum = await showDialog( - context: context, - builder: (context) => const CreateAlbumDialog(), - ); - // wait for the dialog to hide as applying the change may block the UI - await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); - if (newAlbum != null && newAlbum.isNotEmpty) { - Navigator.pop(context, AlbumFilter(newAlbum, source.getAlbumDisplayName(context, newAlbum))); - } - }, - tooltip: context.l10n.createAlbumTooltip, - ), - MenuIconTheme( - child: PopupMenuButton( - itemBuilder: (context) { - return [ - PopupMenuItem( - value: ChipSetAction.configureView, - child: MenuRow(text: context.l10n.menuActionConfigureView, icon: const Icon(AIcons.view)), - ), - ]; - }, - onSelected: (action) async { - // remove focus, if any, to prevent the keyboard from showing up - // after the user is done with the popup menu - FocusManager.instance.primaryFocus?.unfocus(); - - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(Durations.popupMenuAnimation * timeDilation); - actionDelegate.onActionSelected(context, {}, action); - }, - ), - ), - ], - floating: true, ); } + + List _buildActions(BuildContext context) { + return [ + if (moveType != null) + IconButton( + icon: const Icon(AIcons.add), + onPressed: () async { + final newAlbum = await showDialog( + context: context, + builder: (context) => const CreateAlbumDialog(), + ); + // wait for the dialog to hide as applying the change may block the UI + await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); + if (newAlbum != null && newAlbum.isNotEmpty) { + Navigator.pop(context, AlbumFilter(newAlbum, source.getAlbumDisplayName(context, newAlbum))); + } + }, + tooltip: context.l10n.createAlbumTooltip, + ), + MenuIconTheme( + child: PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + value: ChipSetAction.configureView, + child: MenuRow(text: context.l10n.menuActionConfigureView, icon: const Icon(AIcons.view)), + ), + ]; + }, + onSelected: (action) async { + // remove focus, if any, to prevent the keyboard from showing up + // after the user is done with the popup menu + FocusManager.instance.primaryFocus?.unfocus(); + + // wait for the popup menu to hide before proceeding with the action + await Future.delayed(Durations.popupMenuAnimation * timeDilation); + actionDelegate.onActionSelected(context, {}, action); + }, + ), + ), + ]; + } } class _AlbumQueryBar extends StatelessWidget implements PreferredSizeWidget { @@ -207,9 +207,8 @@ class _AlbumQueryBar extends StatelessWidget implements PreferredSizeWidget { static const preferredHeight = kToolbarHeight; const _AlbumQueryBar({ - Key? key, required this.queryNotifier, - }) : super(key: key); + }); @override Size get preferredSize => const Size.fromHeight(preferredHeight); diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index f44836da6..9ebb50c89 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -20,7 +20,7 @@ import 'package:tuple/tuple.dart'; class AlbumListPage extends StatelessWidget { static const routeName = '/albums'; - const AlbumListPage({Key? key}) : super(key: key); + const AlbumListPage({super.key}); @override Widget build(BuildContext context) { 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 72330a527..51b6ab0a9 100644 --- a/lib/widgets/filter_grids/common/action_delegates/album_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/album_set.dart @@ -15,6 +15,7 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/enums.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/action_mixins/entry_storage.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; @@ -173,9 +174,25 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with final showAction = SnackBarAction( label: context.l10n.showButtonLabel, onPressed: () async { - // assume Album page is still the current page when action is triggered - final filter = AlbumFilter(newAlbum, source.getAlbumDisplayName(context, newAlbum)); - context.read().trackItem(FilterGridItem(filter, null), highlightItem: filter); + // local context may be deactivated when action is triggered after navigation + final context = AvesApp.navigatorKey.currentContext; + if (context != null) { + final highlightInfo = context.read(); + final filter = AlbumFilter(newAlbum, source.getAlbumDisplayName(context, newAlbum)); + if (context.currentRouteName == AlbumListPage.routeName) { + highlightInfo.trackItem(FilterGridItem(filter, null), highlightItem: filter); + } else { + highlightInfo.set(filter); + await Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + settings: const RouteSettings(name: AlbumListPage.routeName), + builder: (_) => const AlbumListPage(), + ), + (route) => false, + ); + } + } }, ); showFeedback(context, context.l10n.genericSuccessFeedback, showAction); 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 cc94f0b1a..f6c0b626b 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -15,6 +15,7 @@ import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/search/route.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/filter_editors/cover_selection_dialog.dart'; import 'package:aves/widgets/dialogs/tile_view_dialog.dart'; @@ -52,7 +53,7 @@ abstract class ChipSetActionDelegate with FeedbackMi case ChipSetAction.configureView: return true; case ChipSetAction.select: - return appMode.canSelect && !isSelecting; + return appMode.canSelectFilter && !isSelecting; case ChipSetAction.selectAll: return isSelecting && selectedItemCount < itemCount; case ChipSetAction.selectNone: @@ -246,6 +247,7 @@ abstract class ChipSetActionDelegate with FeedbackMi context, SearchPageRoute( delegate: CollectionSearchDelegate( + searchFieldLabel: context.l10n.searchCollectionFieldHint, source: context.read(), ), ), diff --git a/lib/widgets/filter_grids/common/app_bar.dart b/lib/widgets/filter_grids/common/app_bar.dart index 858b0c7ac..108a0cfd9 100644 --- a/lib/widgets/filter_grids/common/app_bar.dart +++ b/lib/widgets/filter_grids/common/app_bar.dart @@ -4,12 +4,12 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/selection.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/common/animated_icons_fix.dart'; import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/sliver_app_bar_title.dart'; +import 'package:aves/widgets/common/identity/aves_app_bar.dart'; +import 'package:aves/widgets/common/search/route.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:flutter/material.dart'; @@ -23,12 +23,12 @@ class FilterGridAppBar extends StatefulWidget { final bool isEmpty; const FilterGridAppBar({ - Key? key, + super.key, required this.source, required this.title, required this.actionDelegate, required this.isEmpty, - }) : super(key: key); + }); @override State> createState() => _FilterGridAppBarState(); @@ -74,18 +74,23 @@ class _FilterGridAppBarState extends State>>(); final isSelecting = selection.isSelecting; _isSelectingNotifier.value = isSelecting; - return SliverAppBar( - leading: appMode.hasDrawer ? _buildAppBarLeading(isSelecting) : null, - title: SliverAppBarTitleWrapper( - child: _buildAppBarTitle(isSelecting), + return AvesAppBar( + contentHeight: kToolbarHeight, + leading: _buildAppBarLeading( + hasDrawer: appMode.hasDrawer, + isSelecting: isSelecting, ), + title: _buildAppBarTitle(isSelecting), actions: _buildActions(appMode, selection), - titleSpacing: 0, - floating: true, + transitionKey: isSelecting, ); } - Widget _buildAppBarLeading(bool isSelecting) { + Widget _buildAppBarLeading({required bool hasDrawer, required bool isSelecting}) { + if (!hasDrawer) { + return const CloseButton(); + } + VoidCallback? onPressed; String? tooltip; if (isSelecting) { @@ -98,9 +103,8 @@ class _FilterGridAppBarState extends State extends State extends StatelessWidget { final HeroType heroType; const CoveredFilterChip({ - Key? key, + super.key, required this.filter, required this.extent, double? thumbnailExtent, @@ -38,8 +38,7 @@ class CoveredFilterChip extends StatelessWidget { this.banner, this.onTap, this.heroType = HeroType.onTap, - }) : thumbnailExtent = thumbnailExtent ?? extent, - super(key: key); + }) : thumbnailExtent = thumbnailExtent ?? extent; static double tileHeight({required double extent, required double textScaleFactor, required bool showText}) { return extent + infoHeight(extent: extent, textScaleFactor: textScaleFactor, showText: showText); diff --git a/lib/widgets/filter_grids/common/draggable_thumb_label.dart b/lib/widgets/filter_grids/common/draggable_thumb_label.dart index 433c54867..5276ac819 100644 --- a/lib/widgets/filter_grids/common/draggable_thumb_label.dart +++ b/lib/widgets/filter_grids/common/draggable_thumb_label.dart @@ -11,10 +11,10 @@ class FilterDraggableThumbLabel extends StatelessWid final double offsetY; const FilterDraggableThumbLabel({ - Key? key, + super.key, required this.sortFactor, required this.offsetY, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/filter_grids/common/filter_chip_grid_decorator.dart b/lib/widgets/filter_grids/common/filter_chip_grid_decorator.dart index 1f6d68cea..1ed8368c2 100644 --- a/lib/widgets/filter_grids/common/filter_chip_grid_decorator.dart +++ b/lib/widgets/filter_grids/common/filter_chip_grid_decorator.dart @@ -11,13 +11,13 @@ class FilterChipGridDecorator extends StatelessWidget { final QueryTest? applyQuery; final Widget Function() emptyBuilder; final HeroType heroType; + final StreamController _draggableScrollBarEventStreamController = StreamController.broadcast(); - const FilterGridPage({ - Key? key, + FilterGridPage({ + super.key, this.settingsRouteKey, required this.appBar, - this.appBarHeight = kToolbarHeight, + required this.appBarHeight, required this.sections, required this.newFilters, required this.sortFactor, @@ -64,47 +68,70 @@ class FilterGridPage extends StatelessWidget { this.applyQuery, required this.emptyBuilder, required this.heroType, - }) : super(key: key); + }); @override Widget build(BuildContext context) { return MediaQueryDataProvider( - child: Scaffold( - body: WillPopScope( - onWillPop: () { - final selection = context.read>>(); - if (selection.isSelecting) { - selection.browse(); - return SynchronousFuture(false); - } - return SynchronousFuture(true); - }, - child: DoubleBackPopScope( - child: GestureAreaProtectorStack( - child: SafeArea( - bottom: false, - child: FilterGrid( - // key is expected by test driver - key: const Key('filter-grid'), - settingsRouteKey: settingsRouteKey, - appBar: appBar, - appBarHeight: appBarHeight, - sections: sections, - newFilters: newFilters, - sortFactor: sortFactor, - showHeaders: showHeaders, - selectable: selectable, - queryNotifier: queryNotifier, - applyQuery: applyQuery, - emptyBuilder: emptyBuilder, - heroType: heroType, + child: Selector( + selector: (context, s) => s.showBottomNavigationBar, + builder: (context, showBottomNavigationBar, child) { + return NotificationListener( + onNotification: (notification) { + _draggableScrollBarEventStreamController.add(notification.event); + return false; + }, + child: Scaffold( + body: WillPopScope( + onWillPop: () { + final selection = context.read>>(); + if (selection.isSelecting) { + selection.browse(); + return SynchronousFuture(false); + } + return SynchronousFuture(true); + }, + child: DoubleBackPopScope( + child: GestureAreaProtectorStack( + child: SafeArea( + top: false, + bottom: false, + child: Selector( + selector: (context, mq) => mq.padding.top, + builder: (context, mqPaddingTop, child) { + return FilterGrid( + // key is expected by test driver + key: const Key('filter-grid'), + settingsRouteKey: settingsRouteKey, + appBar: appBar, + appBarHeight: mqPaddingTop + appBarHeight, + sections: sections, + newFilters: newFilters, + sortFactor: sortFactor, + showHeaders: showHeaders, + selectable: selectable, + queryNotifier: queryNotifier, + applyQuery: applyQuery, + emptyBuilder: emptyBuilder, + heroType: heroType, + ); + }, + ), + ), + ), ), ), + drawer: const AppDrawer(), + bottomNavigationBar: showBottomNavigationBar + ? AppBottomNavBar( + events: _draggableScrollBarEventStreamController.stream, + ) + : null, + resizeToAvoidBottomInset: false, + extendBody: true, ), - ), - ), - drawer: const AppDrawer(), - resizeToAvoidBottomInset: false, + ); + }, ), ); } @@ -124,7 +151,7 @@ class FilterGrid extends StatefulWidget { final HeroType heroType; const FilterGrid({ - Key? key, + super.key, required this.settingsRouteKey, required this.appBar, required this.appBarHeight, @@ -137,7 +164,7 @@ class FilterGrid extends StatefulWidget { required this.applyQuery, required this.emptyBuilder, required this.heroType, - }) : super(key: key); + }); @override State> createState() => _FilterGridState(); @@ -195,7 +222,7 @@ class _FilterGridContent extends StatelessWidget { final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); _FilterGridContent({ - Key? key, + super.key, required this.appBar, required double appBarHeight, required this.sections, @@ -207,7 +234,7 @@ class _FilterGridContent extends StatelessWidget { required this.applyQuery, required this.emptyBuilder, required this.heroType, - }) : super(key: key) { + }) { _appBarHeightNotifier.value = appBarHeight; } @@ -356,7 +383,7 @@ class _FilterSectionedContentState extends State<_Fi @override void initState() { super.initState(); - WidgetsBinding.instance!.addPostFrameCallback((_) => _checkInitHighlight()); + WidgetsBinding.instance.addPostFrameCallback((_) => _checkInitHighlight()); } @override @@ -380,10 +407,9 @@ class _FilterSectionedContentState extends State<_Fi child: scrollView, ); - final isMainMode = context.select, bool>((vn) => vn.value == AppMode.main); final selector = GridSelectionGestureDetector>( scrollableKey: scrollableKey, - selectable: isMainMode && widget.selectable, + selectable: context.select, bool>((v) => v.value.canSelectFilter) && widget.selectable, items: visibleSections.values.expand((v) => v).toList(), scrollController: scrollController, appBarHeightNotifier: appBarHeightNotifier, @@ -491,27 +517,41 @@ class _FilterScrollView extends StatelessWidget { } Widget _buildDraggableScrollView(ScrollView scrollView) { - return Selector( - selector: (context, mq) => mq.effectiveBottomPadding, - builder: (context, mqPaddingBottom, child) => DraggableScrollbar( - backgroundColor: Colors.white, - scrollThumbHeight: avesScrollThumbHeight, - scrollThumbBuilder: avesScrollThumbBuilder( - height: avesScrollThumbHeight, - backgroundColor: Colors.white, - ), - controller: scrollController, - padding: EdgeInsets.only( - // padding to keep scroll thumb between app bar above and nav bar below - top: appBarHeightNotifier.value, - bottom: mqPaddingBottom, - ), - labelTextBuilder: (offsetY) => FilterDraggableThumbLabel( - sortFactor: sortFactor, - offsetY: offsetY, - ), - child: scrollView, - ), + return ValueListenableBuilder( + valueListenable: appBarHeightNotifier, + builder: (context, appBarHeight, child) { + return Selector( + selector: (context, mq) => mq.effectiveBottomPadding, + builder: (context, mqPaddingBottom, child) { + return Selector( + selector: (context, s) => s.showBottomNavigationBar, + builder: (context, showBottomNavigationBar, child) { + final navBarHeight = showBottomNavigationBar ? AppBottomNavBar.height : 0; + return DraggableScrollbar( + backgroundColor: Colors.white, + scrollThumbSize: Size(avesScrollThumbWidth, avesScrollThumbHeight), + scrollThumbBuilder: avesScrollThumbBuilder( + height: avesScrollThumbHeight, + backgroundColor: Colors.white, + ), + controller: scrollController, + padding: EdgeInsets.only( + // padding to keep scroll thumb between app bar above and nav bar below + top: appBarHeight, + bottom: navBarHeight + mqPaddingBottom, + ), + labelTextBuilder: (offsetY) => FilterDraggableThumbLabel( + sortFactor: sortFactor, + offsetY: offsetY, + ), + crumbTextBuilder: (offsetY) => const SizedBox(), + child: scrollView, + ); + }, + ); + }, + ); + }, ); } @@ -539,6 +579,7 @@ class _FilterScrollView extends StatelessWidget { ) : SectionedListSliver>(); }), + const NavBarPaddingSliver(), const BottomPaddingSliver(), ], ); diff --git a/lib/widgets/filter_grids/common/filter_nav_page.dart b/lib/widgets/filter_grids/common/filter_nav_page.dart index 9746f1c1a..e06f92500 100644 --- a/lib/widgets/filter_grids/common/filter_nav_page.dart +++ b/lib/widgets/filter_grids/common/filter_nav_page.dart @@ -2,6 +2,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/utils/time_utils.dart'; +import 'package:aves/widgets/common/identity/aves_app_bar.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; @@ -21,7 +22,7 @@ class FilterNavigationPage extends StatelessWidget { final Widget Function() emptyBuilder; const FilterNavigationPage({ - Key? key, + super.key, required this.source, required this.title, required this.sortFactor, @@ -30,7 +31,7 @@ class FilterNavigationPage extends StatelessWidget { required this.filterSections, this.newFilters, required this.emptyBuilder, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -43,6 +44,7 @@ class FilterNavigationPage extends StatelessWidget { actionDelegate: actionDelegate, isEmpty: filterSections.isEmpty, ), + appBarHeight: AvesAppBar.appBarHeightForContentHeight(kToolbarHeight), sections: filterSections, newFilters: newFilters ?? {}, sortFactor: sortFactor, diff --git a/lib/widgets/filter_grids/common/filter_tile.dart b/lib/widgets/filter_grids/common/filter_tile.dart index b7d246092..34ece3225 100644 --- a/lib/widgets/filter_grids/common/filter_tile.dart +++ b/lib/widgets/filter_grids/common/filter_tile.dart @@ -22,14 +22,14 @@ class InteractiveFilterTile extends StatefulWidget { final HeroType heroType; const InteractiveFilterTile({ - Key? key, + super.key, required this.gridItem, required this.chipExtent, required this.thumbnailExtent, required this.tileLayout, this.banner, required this.heroType, - }) : super(key: key); + }); @override State> createState() => _InteractiveFilterTileState(); @@ -50,7 +50,8 @@ class _InteractiveFilterTileState extends State>().value; switch (appMode) { case AppMode.main: - case AppMode.pickMediaExternal: + case AppMode.pickSingleMediaExternal: + case AppMode.pickMultipleMediaExternal: final selection = context.read>>(); if (selection.isSelecting) { selection.toggleSelection(gridItem); @@ -88,7 +89,7 @@ class _InteractiveFilterTileState extends State _heroTypeOverride = HeroType.always); } - WidgetsBinding.instance!.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { Navigator.push( context, MaterialPageRoute( @@ -113,7 +114,7 @@ class FilterTile extends StatelessWidget { final HeroType heroType; const FilterTile({ - Key? key, + super.key, required this.gridItem, required this.chipExtent, required this.thumbnailExtent, @@ -123,7 +124,7 @@ class FilterTile extends StatelessWidget { this.highlightable = false, this.onTap, this.heroType = HeroType.never, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/filter_grids/common/list_details.dart b/lib/widgets/filter_grids/common/list_details.dart index f7605a8ec..3a7678d38 100644 --- a/lib/widgets/filter_grids/common/list_details.dart +++ b/lib/widgets/filter_grids/common/list_details.dart @@ -22,10 +22,10 @@ class FilterListDetails extends StatelessWidget { AvesEntry? get entry => gridItem.entry; const FilterListDetails({ - Key? key, + super.key, required this.gridItem, required this.pinned, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/filter_grids/common/list_details_theme.dart b/lib/widgets/filter_grids/common/list_details_theme.dart index 894d160b5..1c0218661 100644 --- a/lib/widgets/filter_grids/common/list_details_theme.dart +++ b/lib/widgets/filter_grids/common/list_details_theme.dart @@ -18,10 +18,10 @@ class FilterListDetailsTheme extends StatelessWidget { static const double titleDetailPadding = 6; const FilterListDetailsTheme({ - Key? key, + super.key, required this.extent, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/filter_grids/common/overlay.dart b/lib/widgets/filter_grids/common/overlay.dart index 48651e9a7..1e479a9d8 100644 --- a/lib/widgets/filter_grids/common/overlay.dart +++ b/lib/widgets/filter_grids/common/overlay.dart @@ -12,11 +12,11 @@ class ChipHighlightOverlay extends StatefulWidget { final BorderRadius borderRadius; const ChipHighlightOverlay({ - Key? key, + super.key, required this.filter, required this.extent, required this.borderRadius, - }) : super(key: key); + }); @override State createState() => _ChipHighlightOverlayState(); diff --git a/lib/widgets/filter_grids/common/section_header.dart b/lib/widgets/filter_grids/common/section_header.dart index 4f83005ea..7a9960510 100644 --- a/lib/widgets/filter_grids/common/section_header.dart +++ b/lib/widgets/filter_grids/common/section_header.dart @@ -6,9 +6,9 @@ class FilterChipSectionHeader extends StatelessWidget { final ChipSectionKey sectionKey; const FilterChipSectionHeader({ - Key? key, + super.key, required this.sectionKey, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -21,6 +21,6 @@ class FilterChipSectionHeader extends StatelessWidget { static double getPreferredHeight(BuildContext context) { final textScaleFactor = MediaQuery.textScaleFactorOf(context); - return SectionHeader.leadingDimension * textScaleFactor + SectionHeader.padding.vertical; + return SectionHeader.leadingSize.height * textScaleFactor + SectionHeader.padding.vertical; } } diff --git a/lib/widgets/filter_grids/common/section_layout.dart b/lib/widgets/filter_grids/common/section_layout.dart index 3e12913b5..832078152 100644 --- a/lib/widgets/filter_grids/common/section_layout.dart +++ b/lib/widgets/filter_grids/common/section_layout.dart @@ -1,5 +1,4 @@ import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/filter_grids/common/section_header.dart'; @@ -8,32 +7,20 @@ import 'package:flutter/material.dart'; class SectionedFilterListLayoutProvider extends SectionedListLayoutProvider> { const SectionedFilterListLayoutProvider({ - Key? key, + super.key, required this.sections, required this.showHeaders, - required double scrollableWidth, - required TileLayout tileLayout, - required int columnCount, - required double spacing, - required double horizontalPadding, - required double tileWidth, - required double tileHeight, - required Widget Function(FilterGridItem gridItem) tileBuilder, - required Duration tileAnimationDelay, - required Widget child, - }) : super( - key: key, - scrollableWidth: scrollableWidth, - tileLayout: tileLayout, - columnCount: columnCount, - spacing: spacing, - horizontalPadding: horizontalPadding, - tileWidth: tileWidth, - tileHeight: tileHeight, - tileBuilder: tileBuilder, - tileAnimationDelay: tileAnimationDelay, - child: child, - ); + 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, + required super.child, + }); @override final Map>> sections; diff --git a/lib/widgets/filter_grids/countries_page.dart b/lib/widgets/filter_grids/countries_page.dart index e99a2f05a..a3da3c50b 100644 --- a/lib/widgets/filter_grids/countries_page.dart +++ b/lib/widgets/filter_grids/countries_page.dart @@ -18,7 +18,7 @@ import 'package:tuple/tuple.dart'; class CountryListPage extends StatelessWidget { static const routeName = '/countries'; - const CountryListPage({Key? key}) : super(key: key); + const CountryListPage({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/filter_grids/tags_page.dart b/lib/widgets/filter_grids/tags_page.dart index 92220bbc8..89091a2ff 100644 --- a/lib/widgets/filter_grids/tags_page.dart +++ b/lib/widgets/filter_grids/tags_page.dart @@ -18,7 +18,7 @@ import 'package:tuple/tuple.dart'; class TagListPage extends StatelessWidget { static const routeName = '/tags'; - const TagListPage({Key? key}) : super(key: key); + const TagListPage({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 34fd98f52..24472508f 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -17,9 +17,10 @@ import 'package:aves/services/viewer_service.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/search/route.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/search/search_delegate.dart'; -import 'package:aves/widgets/search/search_page.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -34,9 +35,9 @@ class HomePage extends StatefulWidget { final Map? intentData; const HomePage({ - Key? key, + super.key, this.intentData, - }) : super(key: key); + }); @override State createState() => _HomePageState(); @@ -47,13 +48,17 @@ class _HomePageState extends State { String? _shortcutRouteName, _shortcutSearchQuery; Set? _shortcutFilters; - static const allowedShortcutRoutes = [CollectionPage.routeName, AlbumListPage.routeName, SearchPage.routeName]; + static const allowedShortcutRoutes = [ + CollectionPage.routeName, + AlbumListPage.routeName, + CollectionSearchDelegate.pageRouteName, + ]; @override void initState() { super.initState(); _setup(); - imageCache!.maximumSizeBytes = 512 * (1 << 20); + imageCache.maximumSizeBytes = 512 * (1 << 20); } @override @@ -95,14 +100,15 @@ class _HomePageState extends State { } break; case 'pick': - appMode = AppMode.pickMediaExternal; // TODO TLAD apply pick mimetype(s) // some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?) String? pickMimeTypes = intentData['mimeType']; - debugPrint('pick mimeType=$pickMimeTypes'); + final multiple = intentData['allowMultiple'] ?? false; + debugPrint('pick mimeType=$pickMimeTypes multiple=$multiple'); + appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal; break; case 'search': - _shortcutRouteName = SearchPage.routeName; + _shortcutRouteName = CollectionSearchDelegate.pageRouteName; _shortcutSearchQuery = intentData['query']; break; default: @@ -121,7 +127,8 @@ class _HomePageState extends State { switch (appMode) { case AppMode.main: - case AppMode.pickMediaExternal: + case AppMode.pickSingleMediaExternal: + case AppMode.pickMultipleMediaExternal: unawaited(GlobalSearch.registerCallback()); unawaited(AnalysisService.registerCallback()); final source = context.read(); @@ -226,11 +233,15 @@ class _HomePageState extends State { String routeName; Set? filters; - if (appMode == AppMode.pickMediaExternal) { - routeName = CollectionPage.routeName; - } else { - routeName = _shortcutRouteName ?? settings.homePage.routeName; - filters = (_shortcutFilters ?? {}).map(CollectionFilter.fromJson).toSet(); + switch (appMode) { + case AppMode.pickSingleMediaExternal: + case AppMode.pickMultipleMediaExternal: + routeName = CollectionPage.routeName; + break; + default: + routeName = _shortcutRouteName ?? settings.homePage.routeName; + filters = (_shortcutFilters ?? {}).map(CollectionFilter.fromJson).toSet(); + break; } final source = context.read(); switch (routeName) { @@ -239,9 +250,10 @@ class _HomePageState extends State { settings: const RouteSettings(name: AlbumListPage.routeName), builder: (_) => const AlbumListPage(), ); - case SearchPage.routeName: + case CollectionSearchDelegate.pageRouteName: return SearchPageRoute( delegate: CollectionSearchDelegate( + searchFieldLabel: context.l10n.searchCollectionFieldHint, source: source, canPop: false, initialQuery: _shortcutSearchQuery, diff --git a/lib/widgets/map/map_info_row.dart b/lib/widgets/map/map_info_row.dart index 9670d26b9..2ae6b55ce 100644 --- a/lib/widgets/map/map_info_row.dart +++ b/lib/widgets/map/map_info_row.dart @@ -7,8 +7,7 @@ import 'package:aves/theme/format.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/map/marker.dart'; -import 'package:decorated_icon/decorated_icon.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -20,9 +19,9 @@ class MapInfoRow extends StatelessWidget { static const double _interRowPadding = 2.0; const MapInfoRow({ - Key? key, + super.key, required this.entryNotifier, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -72,9 +71,8 @@ class _AddressRow extends StatefulWidget { final AvesEntry? entry; const _AddressRow({ - Key? key, required this.entry, - }) : super(key: key); + }); @override State<_AddressRow> createState() => _AddressRowState(); @@ -104,7 +102,7 @@ class _AddressRowState extends State<_AddressRow> { mainAxisSize: MainAxisSize.min, children: [ const SizedBox(width: MapInfoRow.iconPadding), - const DecoratedIcon(AIcons.location, size: MapInfoRow.iconSize), + const Icon(AIcons.location, size: MapInfoRow.iconSize), const SizedBox(width: MapInfoRow.iconPadding), Expanded( child: Container( @@ -161,9 +159,8 @@ class _DateRow extends StatelessWidget { final AvesEntry? entry; const _DateRow({ - Key? key, required this.entry, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -175,7 +172,7 @@ class _DateRow extends StatelessWidget { return Row( children: [ const SizedBox(width: MapInfoRow.iconPadding), - const DecoratedIcon(AIcons.date, size: MapInfoRow.iconSize), + const Icon(AIcons.date, size: MapInfoRow.iconSize), const SizedBox(width: MapInfoRow.iconPadding), Text( dateText, diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 000557b94..955cddb05 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -6,7 +6,6 @@ import 'package:aves/model/filters/coordinate.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/geotiff.dart'; import 'package:aves/model/highlight.dart'; -import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -17,16 +16,15 @@ import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/empty.dart'; -import 'package:aves/widgets/common/map/controller.dart'; import 'package:aves/widgets/common/map/geo_map.dart'; -import 'package:aves/widgets/common/map/theme.dart'; -import 'package:aves/widgets/common/map/zoomed_bounds.dart'; 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/map/map_info_row.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/notifications.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -41,11 +39,11 @@ class MapPage extends StatelessWidget { final MappedGeoTiff? overlayEntry; const MapPage({ - Key? key, + super.key, required this.collection, this.initialEntry, this.overlayEntry, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -78,11 +76,10 @@ class _Content extends StatefulWidget { final MappedGeoTiff? overlayEntry; const _Content({ - Key? key, required this.collection, this.initialEntry, this.overlayEntry, - }) : super(key: key); + }); @override State<_Content> createState() => _ContentState(); @@ -110,7 +107,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin void initState() { super.initState(); - if (settings.infoMapStyle.isGoogleMaps) { + if (settings.infoMapStyle.isHeavy) { _isPageAnimatingNotifier.value = true; Future.delayed(Durations.pageTransitionAnimation * timeDilation).then((_) { if (!mounted) return; @@ -149,7 +146,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin } }); - WidgetsBinding.instance!.addPostFrameCallback((_) => _onOverlayVisibleChange(animate: false)); + WidgetsBinding.instance.addPostFrameCallback((_) => _onOverlayVisibleChange(animate: false)); } @override @@ -176,8 +173,8 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin selector: (context, s) => s.infoMapStyle, builder: (context, mapStyle, child) { late Widget scroller; - if (mapStyle.isGoogleMaps) { - // the Google map widget is too heavy for a smooth resizing animation + if (mapStyle.isHeavy) { + // the map widget is too heavy for a smooth resizing animation // so we just toggle visibility when overlay animation is done scroller = ValueListenableBuilder( valueListenable: _overlayAnimationController, @@ -190,7 +187,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin child: child, ); } else { - // the Leaflet map widget is light enough for a smooth resizing animation + // the map widget is light enough for a smooth resizing animation scroller = FadeTransition( opacity: _scrollerSize, child: SizeTransition( @@ -432,8 +429,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin void _toggleOverlay() => _overlayVisible.value = !_overlayVisible.value; - // TODO TLAD [map] as of Flutter v2.5.1 / google_maps_flutter v2.0.10, toggling overlay changes the size of the map, which is an issue for Google map on Android 12 - // cf https://github.com/flutter/flutter/issues/90556 Future _onOverlayVisibleChange({bool animate = true}) async { if (_overlayVisible.value) { if (animate) { diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/navigation/drawer/app_drawer.dart similarity index 93% rename from lib/widgets/drawer/app_drawer.dart rename to lib/widgets/navigation/drawer/app_drawer.dart index ef0d7512b..559593d15 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/navigation/drawer/app_drawer.dart @@ -17,26 +17,29 @@ 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_logo.dart'; import 'package:aves/widgets/debug/app_debug_page.dart'; -import 'package:aves/widgets/drawer/collection_nav_tile.dart'; -import 'package:aves/widgets/drawer/page_nav_tile.dart'; -import 'package:aves/widgets/drawer/tile.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart'; +import 'package:aves/widgets/navigation/drawer/collection_nav_tile.dart'; +import 'package:aves/widgets/navigation/drawer/page_nav_tile.dart'; +import 'package:aves/widgets/navigation/drawer/tile.dart'; import 'package:aves/widgets/settings/settings_page.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class AppDrawer extends StatelessWidget { +class AppDrawer extends StatefulWidget { // collection loaded in the `CollectionPage`, if any final CollectionLens? currentCollection; const AppDrawer({ - Key? key, + super.key, this.currentCollection, - }) : super(key: key); + }); + + @override + State createState() => _AppDrawerState(); static List getDefaultAlbums(BuildContext context) { final source = context.read(); @@ -47,6 +50,14 @@ class AppDrawer extends StatelessWidget { ..sort(source.compareAlbumsByName); return specialAlbums; } +} + +class _AppDrawerState extends State { + // using the default controller conflicts + // with bottom nav bar primary scroll monitoring + final ScrollController _scrollController = ScrollController(); + + CollectionLens? get currentCollection => widget.currentCollection; @override Widget build(BuildContext context) { @@ -73,6 +84,7 @@ class AppDrawer extends StatelessWidget { builder: (context, mqPaddingBottom, child) { final iconTheme = IconTheme.of(context); return SingleChildScrollView( + controller: _scrollController, // key is expected by test driver key: const Key('drawer-scrollview'), padding: EdgeInsets.only(bottom: mqPaddingBottom), diff --git a/lib/widgets/drawer/collection_nav_tile.dart b/lib/widgets/navigation/drawer/collection_nav_tile.dart similarity index 94% rename from lib/widgets/drawer/collection_nav_tile.dart rename to lib/widgets/navigation/drawer/collection_nav_tile.dart index 8a8a73603..968b7df6f 100644 --- a/lib/widgets/drawer/collection_nav_tile.dart +++ b/lib/widgets/navigation/drawer/collection_nav_tile.dart @@ -5,7 +5,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/drawer/tile.dart'; +import 'package:aves/widgets/navigation/drawer/tile.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -18,15 +18,14 @@ class CollectionNavTile extends StatelessWidget { final bool Function() isSelected; const CollectionNavTile({ - Key? key, + super.key, required this.leading, required this.title, this.trailing, bool? dense, required this.filter, required this.isSelected, - }) : dense = dense ?? false, - super(key: key); + }) : dense = dense ?? false; @override Widget build(BuildContext context) { @@ -78,10 +77,10 @@ class AlbumNavTile extends StatelessWidget { final bool Function() isSelected; const AlbumNavTile({ - Key? key, + super.key, required this.album, required this.isSelected, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/drawer/page_nav_tile.dart b/lib/widgets/navigation/drawer/page_nav_tile.dart similarity index 95% rename from lib/widgets/drawer/page_nav_tile.dart rename to lib/widgets/navigation/drawer/page_nav_tile.dart index 938b43367..e72b6d780 100644 --- a/lib/widgets/drawer/page_nav_tile.dart +++ b/lib/widgets/navigation/drawer/page_nav_tile.dart @@ -1,5 +1,5 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/drawer/tile.dart'; +import 'package:aves/widgets/navigation/drawer/tile.dart'; import 'package:flutter/material.dart'; class PageNavTile extends StatelessWidget { @@ -9,12 +9,12 @@ class PageNavTile extends StatelessWidget { final WidgetBuilder? pageBuilder; const PageNavTile({ - Key? key, + super.key, this.trailing, this.topLevel = true, required this.routeName, required this.pageBuilder, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/navigation/drawer/tile.dart b/lib/widgets/navigation/drawer/tile.dart new file mode 100644 index 000000000..89eb9caaf --- /dev/null +++ b/lib/widgets/navigation/drawer/tile.dart @@ -0,0 +1,81 @@ +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/theme/colors.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/debug/app_debug_page.dart'; +import 'package:aves/widgets/filter_grids/albums_page.dart'; +import 'package:aves/widgets/filter_grids/countries_page.dart'; +import 'package:aves/widgets/filter_grids/tags_page.dart'; +import 'package:aves/widgets/navigation/nav_display.dart'; +import 'package:flutter/material.dart'; + +class DrawerFilterIcon extends StatelessWidget { + final CollectionFilter? filter; + + const DrawerFilterIcon({ + super.key, + required this.filter, + }); + + @override + Widget build(BuildContext context) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + final iconSize = 24 * textScaleFactor; + + final _filter = filter; + if (_filter == null) return Icon(AIcons.allCollection, size: iconSize); + return _filter.iconBuilder(context, iconSize) ?? const SizedBox(); + } +} + +class DrawerFilterTitle extends StatelessWidget { + final CollectionFilter? filter; + + const DrawerFilterTitle({ + super.key, + required this.filter, + }); + + @override + Widget build(BuildContext context) => Text(NavigationDisplay.getFilterTitle(context, filter)); +} + +class DrawerPageIcon extends StatelessWidget { + final String route; + + const DrawerPageIcon({ + super.key, + required this.route, + }); + + @override + Widget build(BuildContext context) { + final icon = NavigationDisplay.getPageIcon(route); + if (icon != null) { + switch (route) { + case AlbumListPage.routeName: + case CountryListPage.routeName: + case TagListPage.routeName: + return Icon(icon); + case AppDebugPage.routeName: + return ShaderMask( + shaderCallback: AvesColorsData.debugGradient.createShader, + blendMode: BlendMode.srcIn, + child: Icon(icon), + ); + } + } + return const SizedBox(); + } +} + +class DrawerPageTitle extends StatelessWidget { + final String route; + + const DrawerPageTitle({ + super.key, + required this.route, + }); + + @override + Widget build(BuildContext context) => Text(NavigationDisplay.getPageTitle(context, route)); +} diff --git a/lib/widgets/navigation/nav_bar/floating.dart b/lib/widgets/navigation/nav_bar/floating.dart new file mode 100644 index 000000000..661568727 --- /dev/null +++ b/lib/widgets/navigation/nav_bar/floating.dart @@ -0,0 +1,122 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; +import 'package:flutter/material.dart'; + +class FloatingNavBar extends StatefulWidget { + final ScrollController? scrollController; + final Stream events; + final double childHeight; + final Widget child; + + const FloatingNavBar({ + super.key, + required this.scrollController, + required this.events, + required this.childHeight, + required this.child, + }); + + @override + State createState() => _FloatingNavBarState(); +} + +class _FloatingNavBarState extends State with SingleTickerProviderStateMixin { + final List _subscriptions = []; + late AnimationController _controller; + late Animation _offsetAnimation; + double? _lastOffset; + bool _isDragging = false; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + _offsetAnimation = Tween( + begin: const Offset(0, 0), + end: const Offset(0, 1), + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.linear, + )) + ..addListener(() { + if (mounted) { + setState(() {}); + } + }); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant FloatingNavBar oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.scrollController != widget.scrollController) { + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(FloatingNavBar widget) { + _lastOffset = null; + widget.scrollController?.addListener(_onScrollChange); + _subscriptions.add(widget.events.listen(_onDraggableScrollBarEvent)); + } + + void _unregisterWidget(FloatingNavBar widget) { + widget.scrollController?.removeListener(_onScrollChange); + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + } + + @override + Widget build(BuildContext context) { + return SlideTransition( + position: _offsetAnimation, + child: widget.child, + ); + } + + void _onScrollChange() { + final scrollController = widget.scrollController; + if (scrollController == null) return; + + final offset = scrollController.offset; + final delta = offset - (_lastOffset ?? offset); + _lastOffset = offset; + + double? newValue; + final childHeight = widget.childHeight; + final after = scrollController.position.extentAfter; + if (after < childHeight && delta > 0) { + newValue = min(_controller.value, after / childHeight); + } else if (!_isDragging || delta > 0) { + newValue = _controller.value + delta / childHeight; + } + if (newValue != null) { + _controller.value = newValue.clamp(0.0, 1.0); + } + } + + void _onDraggableScrollBarEvent(DraggableScrollBarEvent event) { + switch (event) { + case DraggableScrollBarEvent.dragStart: + _isDragging = true; + break; + case DraggableScrollBarEvent.dragEnd: + _isDragging = false; + break; + } + } +} diff --git a/lib/widgets/navigation/nav_bar/nav_bar.dart b/lib/widgets/navigation/nav_bar/nav_bar.dart new file mode 100644 index 000000000..b947607c0 --- /dev/null +++ b/lib/widgets/navigation/nav_bar/nav_bar.dart @@ -0,0 +1,173 @@ +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/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/basic/draggable_scrollbar.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_app_bar.dart'; +import 'package:aves/widgets/filter_grids/albums_page.dart'; +import 'package:aves/widgets/navigation/nav_bar/floating.dart'; +import 'package:aves/widgets/navigation/nav_bar/nav_item.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AppBottomNavBar extends StatefulWidget { + final Stream events; + + // collection loaded in the `CollectionPage`, if any + final CollectionLens? currentCollection; + + static double get height => kBottomNavigationBarHeight + AvesFloatingBar.margin.vertical; + + const AppBottomNavBar({ + super.key, + required this.events, + this.currentCollection, + }); + + @override + State createState() => _AppBottomNavBarState(); +} + +class _AppBottomNavBarState extends State { + String? _lastRoute; + + @override + void initState() { + super.initState(); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant AppBottomNavBar oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(AppBottomNavBar widget) { + widget.currentCollection?.filterChangeNotifier.addListener(_onCollectionFilterChange); + } + + void _unregisterWidget(AppBottomNavBar widget) { + widget.currentCollection?.filterChangeNotifier.removeListener(_onCollectionFilterChange); + } + + @override + Widget build(BuildContext context) { + final showVideo = context.select((s) => !s.hiddenFilters.contains(MimeFilter.video)); + + final items = [ + const AvesBottomNavItem(route: CollectionPage.routeName), + if (showVideo) AvesBottomNavItem(route: CollectionPage.routeName, filter: MimeFilter.video), + const AvesBottomNavItem(route: CollectionPage.routeName, filter: FavouriteFilter.instance), + const AvesBottomNavItem(route: AlbumListPage.routeName), + ]; + + Widget child = AvesFloatingBar( + builder: (context, backgroundColor, child) => BottomNavigationBar( + items: items + .map((item) => BottomNavigationBarItem( + icon: item.icon(context), + label: item.label(context), + )) + .toList(), + onTap: (index) => _goTo(context, items, index), + currentIndex: _getCurrentIndex(context, items), + type: BottomNavigationBarType.fixed, + backgroundColor: backgroundColor, + showSelectedLabels: false, + showUnselectedLabels: false, + ), + ); + + return Hero( + tag: 'nav-bar', + flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { + return MediaQuery.removeViewInsets( + context: context, + removeBottom: true, + child: toHero.widget, + ); + }, + child: FloatingNavBar( + scrollController: PrimaryScrollController.of(context), + events: widget.events, + childHeight: AppBottomNavBar.height + context.select((mq) => mq.effectiveBottomPadding), + child: SafeArea( + child: child, + ), + ), + ); + } + + void _onCollectionFilterChange() => setState(() {}); + + int _getCurrentIndex(BuildContext context, List items) { + // current route may be null during navigation + final currentRoute = context.currentRouteName ?? _lastRoute; + _lastRoute = currentRoute; + + final currentItem = items.firstWhereOrNull((item) { + if (currentRoute != item.route) return false; + + if (item.route != CollectionPage.routeName) return true; + + final currentFilters = widget.currentCollection?.filters; + if (currentFilters == null || currentFilters.length > 1) return false; + return currentFilters.firstOrNull == item.filter; + }); + final currentIndex = currentItem != null ? items.indexOf(currentItem) : 0; + return currentIndex; + } + + void _goTo(BuildContext context, List items, int index) { + final item = items[index]; + final routeName = item.route; + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + settings: RouteSettings(name: routeName), + builder: (context) { + switch (routeName) { + case AlbumListPage.routeName: + return const AlbumListPage(); + case CollectionPage.routeName: + default: + return CollectionPage( + source: context.read(), + filters: {item.filter}, + ); + } + }, + ), + (route) => false, + ); + } +} + +class NavBarPaddingSliver extends StatelessWidget { + const NavBarPaddingSliver({super.key}); + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: Selector( + selector: (context, s) => s.showBottomNavigationBar, + builder: (context, showBottomNavigationBar, child) { + return SizedBox(height: showBottomNavigationBar ? AppBottomNavBar.height : 0); + }, + ), + ); + } +} diff --git a/lib/widgets/navigation/nav_bar/nav_item.dart b/lib/widgets/navigation/nav_bar/nav_item.dart new file mode 100644 index 000000000..fbd916396 --- /dev/null +++ b/lib/widgets/navigation/nav_bar/nav_item.dart @@ -0,0 +1,36 @@ +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/navigation/drawer/tile.dart'; +import 'package:aves/widgets/navigation/nav_display.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +class AvesBottomNavItem extends Equatable { + final String route; + final CollectionFilter? filter; + + @override + List get props => [route, filter]; + + const AvesBottomNavItem({ + required this.route, + this.filter, + }); + + Widget icon(BuildContext context) { + if (route == CollectionPage.routeName) { + return DrawerFilterIcon(filter: filter); + } + + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + final iconSize = 24 * textScaleFactor; + return Icon(NavigationDisplay.getPageIcon(route), size: iconSize); + } + + String label(BuildContext context) { + if (route == CollectionPage.routeName) { + return NavigationDisplay.getFilterTitle(context, filter); + } + return NavigationDisplay.getPageTitle(context, route); + } +} diff --git a/lib/widgets/navigation/nav_display.dart b/lib/widgets/navigation/nav_display.dart new file mode 100644 index 000000000..6edc5971b --- /dev/null +++ b/lib/widgets/navigation/nav_display.dart @@ -0,0 +1,59 @@ +import 'package:aves/model/filters/favourite.dart'; +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/mime.dart'; +import 'package:aves/model/filters/type.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/debug/app_debug_page.dart'; +import 'package:aves/widgets/filter_grids/albums_page.dart'; +import 'package:aves/widgets/filter_grids/countries_page.dart'; +import 'package:aves/widgets/filter_grids/tags_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class NavigationDisplay { + static String getFilterTitle(BuildContext context, CollectionFilter? filter) { + final l10n = context.l10n; + if (filter == null) return l10n.drawerCollectionAll; + if (filter == FavouriteFilter.instance) return l10n.drawerCollectionFavourites; + if (filter == MimeFilter.image) return l10n.drawerCollectionImages; + if (filter == MimeFilter.video) return l10n.drawerCollectionVideos; + if (filter == TypeFilter.animated) return l10n.drawerCollectionAnimated; + if (filter == TypeFilter.motionPhoto) return l10n.drawerCollectionMotionPhotos; + if (filter == TypeFilter.panorama) return l10n.drawerCollectionPanoramas; + if (filter == TypeFilter.raw) return l10n.drawerCollectionRaws; + if (filter == TypeFilter.sphericalVideo) return l10n.drawerCollectionSphericalVideos; + return filter.getLabel(context); + } + + static String getPageTitle(BuildContext context, route) { + final l10n = context.l10n; + switch (route) { + case AlbumListPage.routeName: + return l10n.albumPageTitle; + case CountryListPage.routeName: + return l10n.countryPageTitle; + case TagListPage.routeName: + return l10n.tagPageTitle; + case AppDebugPage.routeName: + return 'Debug'; + default: + return route; + } + } + + static IconData? getPageIcon(String route) { + switch (route) { + case AlbumListPage.routeName: + return AIcons.album; + case CountryListPage.routeName: + return AIcons.location; + case TagListPage.routeName: + return AIcons.tag; + case AppDebugPage.routeName: + return AIcons.debug; + default: + return null; + } + } +} diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index b002b4f42..66ada398b 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -14,24 +14,21 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/ref/mime_types.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/collection_page.dart'; -import 'package:aves/widgets/common/animated_icons_fix.dart'; import 'package:aves/widgets/common/expandable_filter_row.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; -import 'package:aves/widgets/search/search_page.dart'; +import 'package:aves/widgets/common/search/delegate.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; -class CollectionSearchDelegate { +class CollectionSearchDelegate extends AvesSearchDelegate { final CollectionSource source; final CollectionLens? parentCollection; final ValueNotifier _expandedSectionNotifier = ValueNotifier(null); - final bool canPop; + static const pageRouteName = '/search'; static const int searchHistoryCount = 10; static final typeFilters = [ FavouriteFilter.instance, @@ -47,47 +44,18 @@ class CollectionSearchDelegate { ]; CollectionSearchDelegate({ + required super.searchFieldLabel, required this.source, this.parentCollection, - this.canPop = true, + super.canPop, String? initialQuery, - }) { + }) : super( + routeName: pageRouteName, + ) { query = initialQuery ?? ''; } - 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 - return canPop - ? IconButton( - // TODO TLAD [rtl] replace to regular `AnimatedIcon` when this is fixed: https://github.com/flutter/flutter/issues/60521 - icon: AnimatedIconFixIssue60521( - icon: AnimatedIconsFixIssue60521.menu_arrow, - progress: transitionAnimation, - ), - onPressed: () => _goBack(context), - tooltip: MaterialLocalizations.of(context).backButtonTooltip, - ) - : const CloseButton( - onPressed: SystemNavigator.pop, - ); - } - - List buildActions(BuildContext context) { - return [ - if (query.isNotEmpty) - IconButton( - icon: const Icon(AIcons.clear), - onPressed: () { - query = ''; - showSuggestions(context); - }, - tooltip: context.l10n.clearTooltip, - ), - ]; - } - + @override Widget buildSuggestions(BuildContext context) { final upQuery = query.trim().toUpperCase(); bool containQuery(String s) => s.toUpperCase().contains(upQuery); @@ -210,14 +178,15 @@ class CollectionSearchDelegate { ); } + @override Widget buildResults(BuildContext context) { - WidgetsBinding.instance!.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { // `buildResults` is called in the build phase, // so we post the call that will filter the collection // and possibly trigger a rebuild here _select(context, _buildQueryFilter(true)); }); - return const SizedBox.shrink(); + return const SizedBox(); } QueryFilter? _buildQueryFilter(bool colorful) { @@ -227,7 +196,7 @@ class CollectionSearchDelegate { void _select(BuildContext context, CollectionFilter? filter) { if (filter == null) { - _goBack(context); + goBack(context); return; } @@ -249,18 +218,13 @@ class CollectionSearchDelegate { // we post closing the search page after applying the filter selection // so that hero animation target is ready in the `FilterBar`, // even when the target is a child of an `AnimatedList` - WidgetsBinding.instance!.addPostFrameCallback((_) { - _goBack(context); + WidgetsBinding.instance.addPostFrameCallback((_) { + goBack(context); }); } - void _goBack(BuildContext context) { - _clean(); - Navigator.pop(context); - } - void _jumpToCollectionPage(BuildContext context, CollectionFilter filter) { - _clean(); + clean(); Navigator.pushAndRemoveUntil( context, MaterialPageRoute( @@ -273,118 +237,4 @@ class CollectionSearchDelegate { (route) => false, ); } - - void _clean() { - currentBody = null; - focusNode?.unfocus(); - } - - // adapted from Flutter `SearchDelegate` in `/material/search.dart` - - void showResults(BuildContext context) { - focusNode?.unfocus(); - currentBody = SearchBody.results; - } - - void showSuggestions(BuildContext context) { - assert(focusNode != null, '_focusNode must be set by route before showSuggestions is called.'); - focusNode!.requestFocus(); - currentBody = SearchBody.suggestions; - } - - Animation get transitionAnimation => proxyAnimation; - - FocusNode? focusNode; - - final TextEditingController queryTextController = TextEditingController(); - - final ProxyAnimation proxyAnimation = ProxyAnimation(kAlwaysDismissedAnimation); - - String get query => queryTextController.text; - - set query(String value) { - queryTextController.text = value; - } - - final ValueNotifier currentBodyNotifier = ValueNotifier(null); - - SearchBody? get currentBody => currentBodyNotifier.value; - - set currentBody(SearchBody? value) { - currentBodyNotifier.value = value; - } - - SearchPageRoute? route; -} - -// adapted from Flutter `_SearchBody` in `/material/search.dart` -enum SearchBody { suggestions, results } - -// adapted from Flutter `_SearchPageRoute` in `/material/search.dart` -class SearchPageRoute extends PageRoute { - SearchPageRoute({ - required this.delegate, - }) : super(settings: const RouteSettings(name: SearchPage.routeName)) { - assert( - 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.', - ); - delegate.route = this; - } - - final CollectionSearchDelegate delegate; - - @override - Color? get barrierColor => null; - - @override - String? get barrierLabel => null; - - @override - Duration get transitionDuration => const Duration(milliseconds: 300); - - @override - bool get maintainState => false; - - @override - Widget buildTransitions( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, - ) { - return FadeTransition( - opacity: animation, - child: child, - ); - } - - @override - Animation createAnimation() { - final animation = super.createAnimation(); - delegate.proxyAnimation.parent = animation; - return animation; - } - - @override - Widget buildPage( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - ) { - return SearchPage( - delegate: delegate, - animation: animation, - ); - } - - @override - void didComplete(T? result) { - super.didComplete(result); - assert(delegate.route == this); - delegate.route = null; - delegate.currentBody = null; - } } diff --git a/lib/widgets/settings/accessibility/accessibility.dart b/lib/widgets/settings/accessibility/accessibility.dart index 5d44b6853..418b163c6 100644 --- a/lib/widgets/settings/accessibility/accessibility.dart +++ b/lib/widgets/settings/accessibility/accessibility.dart @@ -1,45 +1,57 @@ +import 'dart:async'; + import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/accessibility/time_to_take_action.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class AccessibilitySection extends StatelessWidget { - final ValueNotifier expandedNotifier; - - const AccessibilitySection({ - Key? key, - required this.expandedNotifier, - }) : super(key: key); +class AccessibilitySection extends SettingsSection { + @override + String get key => 'accessibility'; @override - Widget build(BuildContext context) { - return AvesExpansionTile( - leading: SettingsTileLeading( + Widget icon(BuildContext context) => SettingsTileLeading( icon: AIcons.accessibility, color: context.select((v) => v.accessibility), - ), - title: context.l10n.settingsSectionAccessibility, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: [ - SettingsSelectionListTile( - values: AccessibilityAnimations.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => s.accessibilityAnimations, - onSelection: (v) => settings.accessibilityAnimations = v, - tileTitle: context.l10n.settingsRemoveAnimationsTile, - dialogTitle: context.l10n.settingsRemoveAnimationsTitle, - ), - const TimeToTakeActionTile(), - ], - ); - } + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionAccessibility; + + @override + FutureOr> tiles(BuildContext context) => [ + SettingsTileAccessibilityAnimations(), + SettingsTileAccessibilityTimeToTakeAction(), + ]; +} + +class SettingsTileAccessibilityAnimations extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsRemoveAnimationsTile; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: AccessibilityAnimations.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.accessibilityAnimations, + onSelection: (v) => settings.accessibilityAnimations = v, + tileTitle: title(context), + dialogTitle: context.l10n.settingsRemoveAnimationsTitle, + ); +} + +class SettingsTileAccessibilityTimeToTakeAction extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsTimeToTakeActionTile; + + @override + Widget build(BuildContext context) => const TimeToTakeActionTile(); } diff --git a/lib/widgets/settings/accessibility/time_to_take_action.dart b/lib/widgets/settings/accessibility/time_to_take_action.dart index 79db20731..49ffe3a90 100644 --- a/lib/widgets/settings/accessibility/time_to_take_action.dart +++ b/lib/widgets/settings/accessibility/time_to_take_action.dart @@ -7,7 +7,7 @@ import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:flutter/material.dart'; class TimeToTakeActionTile extends StatefulWidget { - const TimeToTakeActionTile({Key? key}) : super(key: key); + const TimeToTakeActionTile({super.key}); @override State createState() => _TimeToTakeActionTileState(); diff --git a/lib/widgets/settings/app_export/selection_dialog.dart b/lib/widgets/settings/app_export/selection_dialog.dart index 34cea2020..986b83603 100644 --- a/lib/widgets/settings/app_export/selection_dialog.dart +++ b/lib/widgets/settings/app_export/selection_dialog.dart @@ -8,11 +8,11 @@ class AppExportItemSelectionDialog extends StatefulWidget { final Set? selectableItems, initialSelection; const AppExportItemSelectionDialog({ - Key? key, + super.key, required this.title, this.selectableItems, this.initialSelection, - }) : super(key: key); + }); @override State createState() => _AppExportItemSelectionDialogState(); diff --git a/lib/widgets/settings/common/quick_actions/action_button.dart b/lib/widgets/settings/common/quick_actions/action_button.dart index a941a1951..9e43a23d6 100644 --- a/lib/widgets/settings/common/quick_actions/action_button.dart +++ b/lib/widgets/settings/common/quick_actions/action_button.dart @@ -7,12 +7,12 @@ class ActionButton extends StatelessWidget { final bool enabled, showCaption; const ActionButton({ - Key? key, + super.key, required this.text, required this.icon, this.enabled = true, this.showCaption = true, - }) : super(key: key); + }); static const padding = 8.0; diff --git a/lib/widgets/settings/common/quick_actions/action_panel.dart b/lib/widgets/settings/common/quick_actions/action_panel.dart index a107fe87d..d2b9cd621 100644 --- a/lib/widgets/settings/common/quick_actions/action_panel.dart +++ b/lib/widgets/settings/common/quick_actions/action_panel.dart @@ -6,10 +6,10 @@ class ActionPanel extends StatelessWidget { final Widget child; const ActionPanel({ - Key? key, + super.key, this.highlight = false, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/settings/common/quick_actions/available_actions.dart b/lib/widgets/settings/common/quick_actions/available_actions.dart index bfa5bc6a1..307ba6fb3 100644 --- a/lib/widgets/settings/common/quick_actions/available_actions.dart +++ b/lib/widgets/settings/common/quick_actions/available_actions.dart @@ -16,7 +16,7 @@ class AvailableActionPanel extends StatelessWidget { final String Function(BuildContext context, T action) actionText; const AvailableActionPanel({ - Key? key, + super.key, required this.allActions, required this.quickActions, required this.quickActionsChangeNotifier, @@ -26,7 +26,7 @@ class AvailableActionPanel extends StatelessWidget { required this.removeQuickAction, required this.actionIcon, required this.actionText, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/settings/common/quick_actions/editor_page.dart b/lib/widgets/settings/common/quick_actions/editor_page.dart index a5cf517ac..5195e25fb 100644 --- a/lib/widgets/settings/common/quick_actions/editor_page.dart +++ b/lib/widgets/settings/common/quick_actions/editor_page.dart @@ -24,7 +24,7 @@ class QuickActionEditorPage extends StatelessWidget { final void Function(List actions) save; const QuickActionEditorPage({ - Key? key, + super.key, required this.title, required this.bannerText, required this.allAvailableActions, @@ -32,7 +32,7 @@ class QuickActionEditorPage extends StatelessWidget { required this.actionText, required this.load, required this.save, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -65,14 +65,14 @@ class QuickActionEditorBody extends StatefulWidget { final void Function(List actions) save; const QuickActionEditorBody({ - Key? key, + super.key, required this.bannerText, required this.allAvailableActions, required this.actionIcon, required this.actionText, required this.load, required this.save, - }) : super(key: key); + }); @override State> createState() => _QuickActionEditorBodyState(); diff --git a/lib/widgets/settings/common/quick_actions/placeholder.dart b/lib/widgets/settings/common/quick_actions/placeholder.dart index 063751754..8024dccd3 100644 --- a/lib/widgets/settings/common/quick_actions/placeholder.dart +++ b/lib/widgets/settings/common/quick_actions/placeholder.dart @@ -4,9 +4,9 @@ class DraggedPlaceholder extends StatelessWidget { final Widget child; const DraggedPlaceholder({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/settings/common/quick_actions/quick_actions.dart b/lib/widgets/settings/common/quick_actions/quick_actions.dart index 5e351048b..5cee3a46b 100644 --- a/lib/widgets/settings/common/quick_actions/quick_actions.dart +++ b/lib/widgets/settings/common/quick_actions/quick_actions.dart @@ -17,7 +17,7 @@ class QuickActionButton extends StatelessWidget { final Widget? child; const QuickActionButton({ - Key? key, + super.key, required this.placement, this.action, required this.panelHighlight, @@ -28,7 +28,7 @@ class QuickActionButton extends StatelessWidget { required this.onTargetLeave, this.draggableFeedbackBuilder, this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/settings/common/tile_leading.dart b/lib/widgets/settings/common/tile_leading.dart index 45f1140cd..55152617a 100644 --- a/lib/widgets/settings/common/tile_leading.dart +++ b/lib/widgets/settings/common/tile_leading.dart @@ -9,10 +9,10 @@ class SettingsTileLeading extends StatelessWidget { final Color color; const SettingsTileLeading({ - Key? key, + super.key, required this.icon, required this.color, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -28,8 +28,9 @@ class SettingsTileLeading extends StatelessWidget { duration: Durations.themeColorModeAnimation, child: DecoratedIcon( icon, - shadows: Theme.of(context).brightness == Brightness.dark ? Constants.embossShadows : null, size: 18, + color: DefaultTextStyle.of(context).style.color, + shadows: Theme.of(context).brightness == Brightness.dark ? Constants.embossShadows : null, ), ); } diff --git a/lib/widgets/settings/common/tiles.dart b/lib/widgets/settings/common/tiles.dart index eee450281..d12564118 100644 --- a/lib/widgets/settings/common/tiles.dart +++ b/lib/widgets/settings/common/tiles.dart @@ -4,6 +4,34 @@ import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +class SettingsSubPageTile extends StatelessWidget { + final String title, routeName; + final WidgetBuilder builder; + + const SettingsSubPageTile({ + super.key, + required this.title, + required this.routeName, + required this.builder, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: routeName), + builder: builder, + ), + ); + }, + ); + } +} + class SettingsSwitchListTile extends StatelessWidget { final bool Function(BuildContext, Settings) selector; final ValueChanged onChanged; @@ -12,13 +40,13 @@ class SettingsSwitchListTile extends StatelessWidget { final Widget? trailing; const SettingsSwitchListTile({ - Key? key, + super.key, required this.selector, required this.onChanged, required this.title, this.subtitle, this.trailing, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -58,7 +86,7 @@ class SettingsSelectionListTile extends StatelessWidget { final TextBuilder? optionSubtitleBuilder; const SettingsSelectionListTile({ - Key? key, + super.key, required this.values, required this.getName, required this.selector, @@ -66,7 +94,7 @@ class SettingsSelectionListTile extends StatelessWidget { required this.tileTitle, required this.dialogTitle, this.optionSubtitleBuilder, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/settings/display/display.dart b/lib/widgets/settings/display/display.dart index 76f153a28..909b7c689 100644 --- a/lib/widgets/settings/display/display.dart +++ b/lib/widgets/settings/display/display.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:aves/model/settings/enums/display_refresh_rate_mode.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/theme_brightness.dart'; @@ -5,53 +7,84 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class DisplaySection extends StatelessWidget { - final ValueNotifier expandedNotifier; - - const DisplaySection({ - Key? key, - required this.expandedNotifier, - }) : super(key: key); +class DisplaySection extends SettingsSection { + @override + String get key => 'display'; @override - Widget build(BuildContext context) { - return AvesExpansionTile( - leading: SettingsTileLeading( + Widget icon(BuildContext context) => SettingsTileLeading( icon: AIcons.display, color: context.select((v) => v.display), - ), - title: context.l10n.settingsSectionDisplay, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: [ - SettingsSelectionListTile( - values: AvesThemeBrightness.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => s.themeBrightness, - onSelection: (v) => settings.themeBrightness = v, - tileTitle: context.l10n.settingsThemeBrightness, - dialogTitle: context.l10n.settingsThemeBrightness, - ), - SettingsSwitchListTile( - selector: (context, s) => s.themeColorMode == AvesThemeColorMode.polychrome, - onChanged: (v) => settings.themeColorMode = v ? AvesThemeColorMode.polychrome : AvesThemeColorMode.monochrome, - title: context.l10n.settingsThemeColorHighlights, - ), - SettingsSelectionListTile( - values: DisplayRefreshRateMode.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => s.displayRefreshRateMode, - onSelection: (v) => settings.displayRefreshRateMode = v, - tileTitle: context.l10n.settingsDisplayRefreshRateModeTile, - dialogTitle: context.l10n.settingsDisplayRefreshRateModeTitle, - ), - ], - ); - } + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionDisplay; + + @override + FutureOr> tiles(BuildContext context) => [ + SettingsTileDisplayThemeBrightness(), + SettingsTileDisplayThemeColorMode(), + SettingsTileDisplayDisplayRefreshRateMode(), + SettingsTileDisplayEnableBlurEffect(), + ]; +} + +class SettingsTileDisplayThemeBrightness extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsThemeBrightness; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: AvesThemeBrightness.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.themeBrightness, + onSelection: (v) => settings.themeBrightness = v, + tileTitle: title(context), + dialogTitle: context.l10n.settingsThemeBrightness, + ); +} + +class SettingsTileDisplayThemeColorMode extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsThemeColorHighlights; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.themeColorMode == AvesThemeColorMode.polychrome, + onChanged: (v) => settings.themeColorMode = v ? AvesThemeColorMode.polychrome : AvesThemeColorMode.monochrome, + title: title(context), + ); +} + +class SettingsTileDisplayDisplayRefreshRateMode extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsDisplayRefreshRateModeTile; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: DisplayRefreshRateMode.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.displayRefreshRateMode, + onSelection: (v) => settings.displayRefreshRateMode = v, + tileTitle: title(context), + dialogTitle: context.l10n.settingsDisplayRefreshRateModeTitle, + ); +} + +class SettingsTileDisplayEnableBlurEffect extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsViewerEnableOverlayBlurEffect; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.enableOverlayBlurEffect, + onChanged: (v) => settings.enableOverlayBlurEffect = v, + title: title(context), + ); } diff --git a/lib/widgets/settings/language/language.dart b/lib/widgets/settings/language/language.dart index 549766333..631981269 100644 --- a/lib/widgets/settings/language/language.dart +++ b/lib/widgets/settings/language/language.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/unit_system.dart'; @@ -6,57 +8,69 @@ import 'package:aves/theme/colors.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/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/language/locale.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class LanguageSection extends StatelessWidget { - final ValueNotifier expandedNotifier; - - const LanguageSection({ - Key? key, - required this.expandedNotifier, - }) : super(key: key); +class LanguageSection extends SettingsSection { + @override + String get key => 'language'; @override - Widget build(BuildContext context) { - final l10n = context.l10n; - return AvesExpansionTile( - // key is expected by test driver - key: const Key('section-language'), - // use a fixed value instead of the title to identify this expansion tile - // so that the tile state is kept when the language is modified - value: 'language', - leading: SettingsTileLeading( + Widget icon(BuildContext context) => SettingsTileLeading( icon: AIcons.language, color: context.select((v) => v.language), - ), - title: l10n.settingsSectionLanguage, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: [ - const LocaleTile(), - SettingsSelectionListTile( - values: CoordinateFormat.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => s.coordinateFormat, - onSelection: (v) => settings.coordinateFormat = v, - tileTitle: l10n.settingsCoordinateFormatTile, - dialogTitle: l10n.settingsCoordinateFormatTitle, - optionSubtitleBuilder: (value) => value.format(l10n, Constants.pointNemo), - ), - SettingsSelectionListTile( - values: UnitSystem.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => s.unitSystem, - onSelection: (v) => settings.unitSystem = v, - tileTitle: l10n.settingsUnitSystemTile, - dialogTitle: l10n.settingsUnitSystemTitle, - ), - ], - ); - } + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionLanguage; + + @override + FutureOr> tiles(BuildContext context) => [ + SettingsTileLanguageLocale(), + SettingsTileLanguageCoordinateFormat(), + SettingsTileLanguageUnitSystem(), + ]; +} + +class SettingsTileLanguageLocale extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsLanguage; + + @override + Widget build(BuildContext context) => const LocaleTile(); +} + +class SettingsTileLanguageCoordinateFormat extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsCoordinateFormatTile; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: CoordinateFormat.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.coordinateFormat, + onSelection: (v) => settings.coordinateFormat = v, + tileTitle: title(context), + dialogTitle: context.l10n.settingsCoordinateFormatTitle, + optionSubtitleBuilder: (value) => value.format(context.l10n, Constants.pointNemo), + ); +} + +class SettingsTileLanguageUnitSystem extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsUnitSystemTile; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: UnitSystem.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.unitSystem, + onSelection: (v) => settings.unitSystem = v, + tileTitle: title(context), + dialogTitle: context.l10n.settingsUnitSystemTitle, + ); } diff --git a/lib/widgets/settings/language/locale.dart b/lib/widgets/settings/language/locale.dart index ca5340d19..882c54f9c 100644 --- a/lib/widgets/settings/language/locale.dart +++ b/lib/widgets/settings/language/locale.dart @@ -15,7 +15,7 @@ import 'package:provider/provider.dart'; class LocaleTile extends StatelessWidget { static const systemLocaleOption = Locale('system'); - const LocaleTile({Key? key}) : super(key: key); + const LocaleTile({super.key}); @override Widget build(BuildContext context) { @@ -56,7 +56,7 @@ class LocaleTile extends StatelessWidget { class LocaleSelectionPage extends StatefulWidget { static const routeName = '/settings/locale'; - const LocaleSelectionPage({Key? key}) : super(key: key); + const LocaleSelectionPage({super.key}); @override State createState() => _LocaleSelectionPageState(); diff --git a/lib/widgets/settings/navigation/confirmation_dialogs.dart b/lib/widgets/settings/navigation/confirmation_dialogs.dart index e45a7890f..970d9cfe3 100644 --- a/lib/widgets/settings/navigation/confirmation_dialogs.dart +++ b/lib/widgets/settings/navigation/confirmation_dialogs.dart @@ -3,30 +3,10 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:flutter/material.dart'; -class ConfirmationDialogTile extends StatelessWidget { - const ConfirmationDialogTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsConfirmationDialogTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: ConfirmationDialogPage.routeName), - builder: (context) => const ConfirmationDialogPage(), - ), - ); - }, - ); - } -} - class ConfirmationDialogPage extends StatelessWidget { static const routeName = '/settings/navigation_confirmation'; - const ConfirmationDialogPage({Key? key}) : super(key: key); + const ConfirmationDialogPage({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/settings/navigation/drawer.dart b/lib/widgets/settings/navigation/drawer.dart index bd7adb4e2..e3dfe27d8 100644 --- a/lib/widgets/settings/navigation/drawer.dart +++ b/lib/widgets/settings/navigation/drawer.dart @@ -1,11 +1,11 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/drawer/app_drawer.dart'; -import 'package:aves/widgets/drawer/tile.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart'; +import 'package:aves/widgets/navigation/drawer/app_drawer.dart'; +import 'package:aves/widgets/navigation/drawer/tile.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/settings/navigation/drawer_tab_albums.dart'; import 'package:aves/widgets/settings/navigation/drawer_tab_fixed.dart'; @@ -13,30 +13,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; -class NavigationDrawerTile extends StatelessWidget { - const NavigationDrawerTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsNavigationDrawerTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: NavigationDrawerEditorPage.routeName), - builder: (context) => const NavigationDrawerEditorPage(), - ), - ); - }, - ); - } -} - class NavigationDrawerEditorPage extends StatefulWidget { static const routeName = '/settings/navigation_drawer'; - const NavigationDrawerEditorPage({Key? key}) : super(key: key); + const NavigationDrawerEditorPage({super.key}); @override State createState() => _NavigationDrawerEditorPageState(); diff --git a/lib/widgets/settings/navigation/drawer_editor_banner.dart b/lib/widgets/settings/navigation/drawer_editor_banner.dart index 3ae27aee6..bd74132bf 100644 --- a/lib/widgets/settings/navigation/drawer_editor_banner.dart +++ b/lib/widgets/settings/navigation/drawer_editor_banner.dart @@ -3,7 +3,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; class DrawerEditorBanner extends StatelessWidget { - const DrawerEditorBanner({Key? key}) : super(key: key); + const DrawerEditorBanner({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/settings/navigation/drawer_tab_albums.dart b/lib/widgets/settings/navigation/drawer_tab_albums.dart index 187d542f8..2708ef157 100644 --- a/lib/widgets/settings/navigation/drawer_tab_albums.dart +++ b/lib/widgets/settings/navigation/drawer_tab_albums.dart @@ -3,8 +3,8 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons.dart'; -import 'package:aves/widgets/drawer/tile.dart'; import 'package:aves/widgets/filter_grids/album_pick.dart'; +import 'package:aves/widgets/navigation/drawer/tile.dart'; import 'package:aves/widgets/settings/navigation/drawer_editor_banner.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -13,9 +13,9 @@ class DrawerAlbumTab extends StatefulWidget { final List items; const DrawerAlbumTab({ - Key? key, + super.key, required this.items, - }) : super(key: key); + }); @override State createState() => _DrawerAlbumTabState(); diff --git a/lib/widgets/settings/navigation/drawer_tab_fixed.dart b/lib/widgets/settings/navigation/drawer_tab_fixed.dart index abc9f5110..0ee54de7b 100644 --- a/lib/widgets/settings/navigation/drawer_tab_fixed.dart +++ b/lib/widgets/settings/navigation/drawer_tab_fixed.dart @@ -12,12 +12,12 @@ class DrawerFixedListTab extends StatefulWidget { final ItemWidgetBuilder title; const DrawerFixedListTab({ - Key? key, + super.key, required this.items, required this.visibleItems, required this.leading, required this.title, - }) : super(key: key); + }); @override State> createState() => _DrawerFixedListTabState(); diff --git a/lib/widgets/settings/navigation/navigation.dart b/lib/widgets/settings/navigation/navigation.dart index 9602fb286..ecbd33b57 100644 --- a/lib/widgets/settings/navigation/navigation.dart +++ b/lib/widgets/settings/navigation/navigation.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/home_page.dart'; import 'package:aves/model/settings/enums/screen_on.dart'; @@ -5,57 +7,112 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/navigation/confirmation_dialogs.dart'; import 'package:aves/widgets/settings/navigation/drawer.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class NavigationSection extends StatelessWidget { - final ValueNotifier expandedNotifier; - - const NavigationSection({ - Key? key, - required this.expandedNotifier, - }) : super(key: key); +class NavigationSection extends SettingsSection { + @override + String get key => 'navigation'; @override - Widget build(BuildContext context) { - return AvesExpansionTile( - leading: SettingsTileLeading( + Widget icon(BuildContext context) => SettingsTileLeading( icon: AIcons.home, color: context.select((v) => v.navigation), - ), - title: context.l10n.settingsSectionNavigation, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: [ - SettingsSelectionListTile( - values: HomePageSetting.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => s.homePage, - onSelection: (v) => settings.homePage = v, - tileTitle: context.l10n.settingsHome, - dialogTitle: context.l10n.settingsHome, - ), - const NavigationDrawerTile(), - const ConfirmationDialogTile(), - SettingsSelectionListTile( - values: KeepScreenOn.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => s.keepScreenOn, - onSelection: (v) => settings.keepScreenOn = v, - tileTitle: context.l10n.settingsKeepScreenOnTile, - dialogTitle: context.l10n.settingsKeepScreenOnTitle, - ), - SettingsSwitchListTile( - selector: (context, s) => s.mustBackTwiceToExit, - onChanged: (v) => settings.mustBackTwiceToExit = v, - title: context.l10n.settingsDoubleBackExit, - ), - ], - ); - } + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionNavigation; + + @override + FutureOr> tiles(BuildContext context) => [ + SettingsTileNavigationHomePage(), + SettingsTileShowBottomNavigationBar(), + SettingsTileNavigationDrawer(), + SettingsTileNavigationConfirmationDialog(), + SettingsTileNavigationKeepScreenOn(), + SettingsTileNavigationDoubleBackExit(), + ]; +} + +class SettingsTileNavigationHomePage extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsHome; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: HomePageSetting.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.homePage, + onSelection: (v) => settings.homePage = v, + tileTitle: title(context), + dialogTitle: context.l10n.settingsHome, + ); +} + +class SettingsTileShowBottomNavigationBar extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsShowBottomNavigationBar; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.showBottomNavigationBar, + onChanged: (v) => settings.showBottomNavigationBar = v, + title: title(context), + ); +} + +class SettingsTileNavigationDrawer extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsNavigationDrawerTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: NavigationDrawerEditorPage.routeName, + builder: (context) => const NavigationDrawerEditorPage(), + ); +} + +class SettingsTileNavigationConfirmationDialog extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsConfirmationDialogTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: ConfirmationDialogPage.routeName, + builder: (context) => const ConfirmationDialogPage(), + ); +} + +class SettingsTileNavigationKeepScreenOn extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsKeepScreenOnTile; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: KeepScreenOn.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.keepScreenOn, + onSelection: (v) => settings.keepScreenOn = v, + tileTitle: title(context), + dialogTitle: context.l10n.settingsKeepScreenOnTitle, + ); +} + +class SettingsTileNavigationDoubleBackExit extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsDoubleBackExit; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.mustBackTwiceToExit, + onChanged: (v) => settings.mustBackTwiceToExit = v, + title: title(context), + ); } diff --git a/lib/widgets/settings/privacy/access_grants.dart b/lib/widgets/settings/privacy/access_grants.dart index 7355c4175..bd63836d6 100644 --- a/lib/widgets/settings/privacy/access_grants.dart +++ b/lib/widgets/settings/privacy/access_grants.dart @@ -5,30 +5,10 @@ import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:flutter/material.dart'; -class StorageAccessTile extends StatelessWidget { - const StorageAccessTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsStorageAccessTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: StorageAccessPage.routeName), - builder: (context) => const StorageAccessPage(), - ), - ); - }, - ); - } -} - class StorageAccessPage extends StatefulWidget { static const routeName = '/settings/storage_access'; - const StorageAccessPage({Key? key}) : super(key: key); + const StorageAccessPage({super.key}); @override State createState() => _StorageAccessPageState(); @@ -110,7 +90,7 @@ class _StorageAccessPageState extends State { } class _Header extends StatelessWidget { - const _Header({Key? key}) : super(key: key); + const _Header(); @override Widget build(BuildContext context) { diff --git a/lib/widgets/settings/privacy/file_picker/crumb_line.dart b/lib/widgets/settings/privacy/file_picker/crumb_line.dart index 6a1c79e8a..e38838d64 100644 --- a/lib/widgets/settings/privacy/file_picker/crumb_line.dart +++ b/lib/widgets/settings/privacy/file_picker/crumb_line.dart @@ -8,10 +8,10 @@ class CrumbLine extends StatefulWidget { final void Function(String path) onTap; const CrumbLine({ - Key? key, + super.key, required this.directory, required this.onTap, - }) : super(key: key); + }); @override State createState() => _CrumbLineState(); @@ -27,7 +27,7 @@ class _CrumbLineState extends State { super.didUpdateWidget(oldWidget); if (widget.directory.relativeDir.length > oldWidget.directory.relativeDir.length) { // scroll to show last crumb - WidgetsBinding.instance!.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { final extent = _controller.position.maxScrollExtent; _controller.animateTo( extent, @@ -54,7 +54,6 @@ class _CrumbLineState extends State { child: ListView.builder( scrollDirection: Axis.horizontal, controller: _controller, - physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 8), itemBuilder: (context, index) { Widget _buildText(String text) => Padding( diff --git a/lib/widgets/settings/privacy/file_picker/file_picker.dart b/lib/widgets/settings/privacy/file_picker/file_picker.dart index dd2ef7347..349ca7ab5 100644 --- a/lib/widgets/settings/privacy/file_picker/file_picker.dart +++ b/lib/widgets/settings/privacy/file_picker/file_picker.dart @@ -20,7 +20,7 @@ import 'package:flutter/scheduler.dart'; class FilePicker extends StatefulWidget { static const routeName = '/file_picker'; - const FilePicker({Key? key}) : super(key: key); + const FilePicker({super.key}); @override State createState() => _FilePickerState(); diff --git a/lib/widgets/settings/privacy/hidden_items.dart b/lib/widgets/settings/privacy/hidden_items.dart index 7073702d1..8ee06d60b 100644 --- a/lib/widgets/settings/privacy/hidden_items.dart +++ b/lib/widgets/settings/privacy/hidden_items.dart @@ -14,30 +14,10 @@ import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; -class HiddenItemsTile extends StatelessWidget { - const HiddenItemsTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsHiddenItemsTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: HiddenItemsPage.routeName), - builder: (context) => const HiddenItemsPage(), - ), - ); - }, - ); - } -} - class HiddenItemsPage extends StatelessWidget { static const routeName = '/settings/hidden_items'; - const HiddenItemsPage({Key? key}) : super(key: key); + const HiddenItemsPage({super.key}); @override Widget build(BuildContext context) { @@ -75,7 +55,7 @@ class HiddenItemsPage extends StatelessWidget { } class _HiddenFilters extends StatelessWidget { - const _HiddenFilters({Key? key}) : super(key: key); + const _HiddenFilters(); @override Widget build(BuildContext context) { @@ -129,7 +109,7 @@ class _HiddenFilters extends StatelessWidget { } class _HiddenPaths extends StatelessWidget { - const _HiddenPaths({Key? key}) : super(key: key); + const _HiddenPaths(); @override Widget build(BuildContext context) { @@ -189,7 +169,7 @@ class _HiddenPaths extends StatelessWidget { class _Banner extends StatelessWidget { final String bannerText; - const _Banner({Key? key, required this.bannerText}) : super(key: key); + const _Banner({required this.bannerText}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/settings/privacy/privacy.dart b/lib/widgets/settings/privacy/privacy.dart index b37bc1abb..c5be45b24 100644 --- a/lib/widgets/settings/privacy/privacy.dart +++ b/lib/widgets/settings/privacy/privacy.dart @@ -1,74 +1,126 @@ +import 'dart:async'; + import 'package:aves/app_flavor.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/privacy/access_grants.dart'; import 'package:aves/widgets/settings/privacy/hidden_items.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class PrivacySection extends StatelessWidget { - final ValueNotifier expandedNotifier; - - const PrivacySection({ - Key? key, - required this.expandedNotifier, - }) : super(key: key); +class PrivacySection extends SettingsSection { + @override + String get key => 'privacy'; @override - Widget build(BuildContext context) { - final canEnableErrorReporting = context.select((v) => v.canEnableErrorReporting); - - return AvesExpansionTile( - leading: SettingsTileLeading( + Widget icon(BuildContext context) => SettingsTileLeading( icon: AIcons.privacy, color: context.select((v) => v.privacy), - ), - title: context.l10n.settingsSectionPrivacy, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: [ - SettingsSwitchListTile( - selector: (context, s) => s.isInstalledAppAccessAllowed, - onChanged: (v) => settings.isInstalledAppAccessAllowed = v, - title: context.l10n.settingsAllowInstalledAppAccess, - subtitle: context.l10n.settingsAllowInstalledAppAccessSubtitle, - ), - if (canEnableErrorReporting) - SettingsSwitchListTile( - selector: (context, s) => s.isErrorReportingAllowed, - onChanged: (v) => settings.isErrorReportingAllowed = v, - title: context.l10n.settingsAllowErrorReporting, - ), - SettingsSwitchListTile( - selector: (context, s) => s.saveSearchHistory, - onChanged: (v) { - settings.saveSearchHistory = v; - if (!v) { - settings.searchHistory = []; - } - }, - title: context.l10n.settingsSaveSearchHistory, - ), - SettingsSwitchListTile( - selector: (context, s) => s.enableBin, - onChanged: (v) { - settings.enableBin = v; - if (!v) { - settings.searchHistory = []; - } - }, - title: context.l10n.settingsEnableBin, - subtitle: context.l10n.settingsEnableBinSubtitle, - ), - const HiddenItemsTile(), - if (device.canGrantDirectoryAccess) const StorageAccessTile(), - ], - ); + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionPrivacy; + + @override + FutureOr> tiles(BuildContext context) async { + final canEnableErrorReporting = context.select((v) => v.canEnableErrorReporting); + return [ + SettingsTilePrivacyAllowInstalledAppAccess(), + if (canEnableErrorReporting) SettingsTilePrivacyAllowErrorReporting(), + SettingsTilePrivacySaveSearchHistory(), + SettingsTilePrivacyEnableBin(), + SettingsTilePrivacyHiddenItems(), + if (device.canGrantDirectoryAccess) SettingsTilePrivacyStorageAccess(), + ]; } } + +class SettingsTilePrivacyAllowInstalledAppAccess extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsAllowInstalledAppAccess; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.isInstalledAppAccessAllowed, + onChanged: (v) => settings.isInstalledAppAccessAllowed = v, + title: title(context), + subtitle: context.l10n.settingsAllowInstalledAppAccessSubtitle, + ); +} + +class SettingsTilePrivacyAllowErrorReporting extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsAllowErrorReporting; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.isErrorReportingAllowed, + onChanged: (v) => settings.isErrorReportingAllowed = v, + title: title(context), + ); +} + +class SettingsTilePrivacySaveSearchHistory extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsSaveSearchHistory; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.saveSearchHistory, + onChanged: (v) { + settings.saveSearchHistory = v; + if (!v) { + settings.searchHistory = []; + } + }, + title: title(context), + ); +} + +class SettingsTilePrivacyEnableBin extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsEnableBin; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.enableBin, + onChanged: (v) { + settings.enableBin = v; + if (!v) { + settings.searchHistory = []; + } + }, + title: title(context), + subtitle: context.l10n.settingsEnableBinSubtitle, + ); +} + +class SettingsTilePrivacyHiddenItems extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsHiddenItemsTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: HiddenItemsPage.routeName, + builder: (context) => const HiddenItemsPage(), + ); +} + +class SettingsTilePrivacyStorageAccess extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsStorageAccessTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: StorageAccessPage.routeName, + builder: (context) => const StorageAccessPage(), + ); +} diff --git a/lib/widgets/settings/settings_definition.dart b/lib/widgets/settings/settings_definition.dart new file mode 100644 index 000000000..e0f7b7724 --- /dev/null +++ b/lib/widgets/settings/settings_definition.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:flutter/widgets.dart'; + +abstract class SettingsSection { + String get key; + + Widget icon(BuildContext context); + + String title(BuildContext context); + + FutureOr> tiles(BuildContext context); + + Widget build(BuildContext context, ValueNotifier expandedNotifier) { + return FutureBuilder>( + future: Future.value(tiles(context)), + builder: (context, snapshot) { + final tiles = snapshot.data; + if (tiles == null) return const SizedBox(); + + return AvesExpansionTile( + // key is expected by test driver + key: Key('section-$key'), + // use a fixed value instead of the title to identify this expansion tile + // so that the tile state is kept when the language is modified + value: key, + leading: icon(context), + title: title(context), + expandedNotifier: expandedNotifier, + showHighlight: false, + children: tiles.map((v) => v.build(context)).toList(), + ); + }, + ); + } +} + +abstract class SettingsTile { + String title(BuildContext context); + + Widget build(BuildContext context); +} diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index 405d9ebe7..37cc46737 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -9,11 +9,13 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/search/route.dart'; import 'package:aves/widgets/settings/accessibility/accessibility.dart'; import 'package:aves/widgets/settings/app_export/items.dart'; import 'package:aves/widgets/settings/app_export/selection_dialog.dart'; @@ -21,6 +23,8 @@ import 'package:aves/widgets/settings/display/display.dart'; import 'package:aves/widgets/settings/language/language.dart'; import 'package:aves/widgets/settings/navigation/navigation.dart'; import 'package:aves/widgets/settings/privacy/privacy.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; +import 'package:aves/widgets/settings/settings_search.dart'; import 'package:aves/widgets/settings/thumbnails/thumbnails.dart'; import 'package:aves/widgets/settings/video/video.dart'; import 'package:aves/widgets/settings/viewer/viewer.dart'; @@ -34,7 +38,7 @@ import 'package:provider/provider.dart'; class SettingsPage extends StatefulWidget { static const routeName = '/settings'; - const SettingsPage({Key? key}) : super(key: key); + const SettingsPage({super.key}); @override State createState() => _SettingsPageState(); @@ -43,6 +47,17 @@ class SettingsPage extends StatefulWidget { class _SettingsPageState extends State with FeedbackMixin { final ValueNotifier _expandedNotifier = ValueNotifier(null); + static final List sections = [ + NavigationSection(), + ThumbnailsSection(), + ViewerSection(), + VideoSection(), + PrivacySection(), + AccessibilitySection(), + DisplaySection(), + LanguageSection(), + ]; + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -50,8 +65,16 @@ class _SettingsPageState extends State with FeedbackMixin { return MediaQueryDataProvider( child: Scaffold( appBar: AppBar( - title: Text(context.l10n.settingsPageTitle), + title: InteractiveAppBarTitle( + onTap: () => _goToSearch(context), + child: Text(context.l10n.settingsPageTitle), + ), actions: [ + IconButton( + icon: const Icon(AIcons.search), + onPressed: () => _goToSearch(context), + tooltip: MaterialLocalizations.of(context).searchFieldLabel, + ), MenuIconTheme( child: PopupMenuButton( itemBuilder: (context) { @@ -100,16 +123,7 @@ class _SettingsPageState extends State with FeedbackMixin { child: child, ), ), - children: [ - NavigationSection(expandedNotifier: _expandedNotifier), - ThumbnailsSection(expandedNotifier: _expandedNotifier), - ViewerSection(expandedNotifier: _expandedNotifier), - VideoSection(expandedNotifier: _expandedNotifier), - PrivacySection(expandedNotifier: _expandedNotifier), - AccessibilitySection(expandedNotifier: _expandedNotifier), - DisplaySection(expandedNotifier: _expandedNotifier), - LanguageSection(expandedNotifier: _expandedNotifier), - ], + children: sections.map((v) => v.build(context, _expandedNotifier)).toList(), ), ); }), @@ -206,4 +220,16 @@ class _SettingsPageState extends State with FeedbackMixin { break; } } + + void _goToSearch(BuildContext context) { + Navigator.push( + context, + SearchPageRoute( + delegate: SettingsSearchDelegate( + searchFieldLabel: context.l10n.settingsSearchFieldLabel, + sections: sections, + ), + ), + ); + } } diff --git a/lib/widgets/settings/settings_search.dart b/lib/widgets/settings/settings_search.dart new file mode 100644 index 000000000..7c92ea90a --- /dev/null +++ b/lib/widgets/settings/settings_search.dart @@ -0,0 +1,84 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; +import 'package:aves/widgets/common/identity/highlight_title.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/search/delegate.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +class SettingsSearchDelegate extends AvesSearchDelegate { + final List sections; + + static const pageRouteName = '/settings/search'; + + SettingsSearchDelegate({ + required super.searchFieldLabel, + required this.sections, + }) : super( + routeName: pageRouteName, + ); + + @override + Widget buildSuggestions(BuildContext context) { + final upQuery = query.toUpperCase().trim(); + if (upQuery.isEmpty) return const SizedBox(); + + bool testKey(String key) => key.toUpperCase().contains(upQuery); + + final loader = Future.wait(sections.map((section) async { + final allTiles = await section.tiles(context); + final filteredTiles = testKey(section.title(context)) ? allTiles : allTiles.where((v) => testKey(v.title(context))).toList(); + if (filteredTiles.isEmpty) return null; + + return (context) { + return [ + Padding( + // match header layout in Settings page + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 13), + child: Row( + children: [ + section.icon(context), + const SizedBox(width: 8), + Expanded( + child: HighlightTitle( + title: section.title(context), + showHighlight: false, + ), + ), + ], + ), + ), + ...filteredTiles.map((v) => v.build(context)), + ]; + }; + })); + + return MediaQueryDataProvider( + child: SafeArea( + child: FutureBuilder Function(BuildContext)?>>( + future: loader, + builder: (context, snapshot) { + final loaders = snapshot.data; + if (loaders == null) return const SizedBox(); + + final children = loaders.whereNotNull().expand((builder) => builder(context)).toList(); + return children.isEmpty + ? EmptyContent( + icon: AIcons.settings, + text: context.l10n.settingsSearchEmpty, + ) + : ListView( + padding: const EdgeInsets.all(8), + children: children, + ); + }, + ), + ), + ); + } + + @override + Widget buildResults(BuildContext context) => buildSuggestions(context); +} diff --git a/lib/widgets/settings/thumbnails/collection_actions_editor.dart b/lib/widgets/settings/thumbnails/collection_actions_editor.dart index a16a8977e..7b6474159 100644 --- a/lib/widgets/settings/thumbnails/collection_actions_editor.dart +++ b/lib/widgets/settings/thumbnails/collection_actions_editor.dart @@ -6,30 +6,10 @@ import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart'; import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; -class CollectionActionsTile extends StatelessWidget { - const CollectionActionsTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsCollectionQuickActionsTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: CollectionActionEditorPage.routeName), - builder: (context) => const CollectionActionEditorPage(), - ), - ); - }, - ); - } -} - class CollectionActionEditorPage extends StatelessWidget { static const routeName = '/settings/collection_actions'; - const CollectionActionEditorPage({Key? key}) : super(key: key); + const CollectionActionEditorPage({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/settings/thumbnails/overlay.dart b/lib/widgets/settings/thumbnails/overlay.dart new file mode 100644 index 000000000..72ea7e997 --- /dev/null +++ b/lib/widgets/settings/thumbnails/overlay.dart @@ -0,0 +1,106 @@ +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/colors.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_icons.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ThumbnailOverlayPage extends StatelessWidget { + static const routeName = '/settings/thumbnail_overlay'; + + const ThumbnailOverlayPage({super.key}); + + @override + Widget build(BuildContext context) { + final iconSize = IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context); + final iconColor = context.select((v) => v.neutral); + + return Scaffold( + appBar: AppBar( + title: Text(context.l10n.settingsThumbnailOverlayTitle), + ), + body: SafeArea( + child: ListView( + children: [ + SettingsSwitchListTile( + selector: (context, s) => s.showThumbnailFavourite, + onChanged: (v) => settings.showThumbnailFavourite = v, + title: context.l10n.settingsThumbnailShowFavouriteIcon, + trailing: Padding( + padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - FavouriteIcon.scale) / 2), + child: Icon( + AIcons.favourite, + size: iconSize * FavouriteIcon.scale, + color: iconColor, + ), + ), + ), + SettingsSwitchListTile( + selector: (context, s) => s.showThumbnailTag, + onChanged: (v) => settings.showThumbnailTag = v, + title: context.l10n.settingsThumbnailShowTagIcon, + trailing: Padding( + padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - TagIcon.scale) / 2), + child: Icon( + AIcons.tag, + size: iconSize * TagIcon.scale, + color: iconColor, + ), + ), + ), + SettingsSwitchListTile( + selector: (context, s) => s.showThumbnailLocation, + onChanged: (v) => settings.showThumbnailLocation = v, + title: context.l10n.settingsThumbnailShowLocationIcon, + trailing: Icon( + AIcons.location, + size: iconSize, + color: iconColor, + ), + ), + SettingsSwitchListTile( + selector: (context, s) => s.showThumbnailMotionPhoto, + onChanged: (v) => settings.showThumbnailMotionPhoto = v, + title: context.l10n.settingsThumbnailShowMotionPhotoIcon, + trailing: Padding( + padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - MotionPhotoIcon.scale) / 2), + child: Icon( + AIcons.motionPhoto, + size: iconSize * MotionPhotoIcon.scale, + color: iconColor, + ), + ), + ), + SettingsSwitchListTile( + selector: (context, s) => s.showThumbnailRating, + onChanged: (v) => settings.showThumbnailRating = v, + title: context.l10n.settingsThumbnailShowRating, + trailing: Icon( + AIcons.rating, + size: iconSize, + color: iconColor, + ), + ), + SettingsSwitchListTile( + selector: (context, s) => s.showThumbnailRaw, + onChanged: (v) => settings.showThumbnailRaw = v, + title: context.l10n.settingsThumbnailShowRawIcon, + trailing: Icon( + AIcons.raw, + size: iconSize, + color: iconColor, + ), + ), + SettingsSwitchListTile( + selector: (context, s) => s.showThumbnailVideoDuration, + onChanged: (v) => settings.showThumbnailVideoDuration = v, + title: context.l10n.settingsThumbnailShowVideoDuration, + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/settings/thumbnails/thumbnails.dart b/lib/widgets/settings/thumbnails/thumbnails.dart index d12a14d4f..60f2c3729 100644 --- a/lib/widgets/settings/thumbnails/thumbnails.dart +++ b/lib/widgets/settings/thumbnails/thumbnails.dart @@ -1,99 +1,54 @@ -import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; -import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:aves/widgets/settings/thumbnails/collection_actions_editor.dart'; +import 'package:aves/widgets/settings/thumbnails/overlay.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class ThumbnailsSection extends StatelessWidget { - final ValueNotifier expandedNotifier; - - const ThumbnailsSection({ - Key? key, - required this.expandedNotifier, - }) : super(key: key); +class ThumbnailsSection extends SettingsSection { + @override + String get key => 'thumbnails'; @override - Widget build(BuildContext context) { - final iconSize = IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context); - final iconColor = context.select((v) => v.neutral); - return AvesExpansionTile( - leading: SettingsTileLeading( + Widget icon(BuildContext context) => SettingsTileLeading( icon: AIcons.grid, color: context.select((v) => v.thumbnails), - ), - title: context.l10n.settingsSectionThumbnails, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: [ - const CollectionActionsTile(), - SettingsSwitchListTile( - selector: (context, s) => s.showThumbnailFavourite, - onChanged: (v) => settings.showThumbnailFavourite = v, - title: context.l10n.settingsThumbnailShowFavouriteIcon, - trailing: Padding( - padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - FavouriteIcon.scale) / 2), - child: Icon( - AIcons.favourite, - size: iconSize * FavouriteIcon.scale, - color: iconColor, - ), - ), - ), - SettingsSwitchListTile( - selector: (context, s) => s.showThumbnailLocation, - onChanged: (v) => settings.showThumbnailLocation = v, - title: context.l10n.settingsThumbnailShowLocationIcon, - trailing: Icon( - AIcons.location, - size: iconSize, - color: iconColor, - ), - ), - SettingsSwitchListTile( - selector: (context, s) => s.showThumbnailMotionPhoto, - onChanged: (v) => settings.showThumbnailMotionPhoto = v, - title: context.l10n.settingsThumbnailShowMotionPhotoIcon, - trailing: Padding( - padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - MotionPhotoIcon.scale) / 2), - child: Icon( - AIcons.motionPhoto, - size: iconSize * MotionPhotoIcon.scale, - color: iconColor, - ), - ), - ), - SettingsSwitchListTile( - selector: (context, s) => s.showThumbnailRating, - onChanged: (v) => settings.showThumbnailRating = v, - title: context.l10n.settingsThumbnailShowRating, - trailing: Icon( - AIcons.rating, - size: iconSize, - color: iconColor, - ), - ), - SettingsSwitchListTile( - selector: (context, s) => s.showThumbnailRaw, - onChanged: (v) => settings.showThumbnailRaw = v, - title: context.l10n.settingsThumbnailShowRawIcon, - trailing: Icon( - AIcons.raw, - size: iconSize, - color: iconColor, - ), - ), - SettingsSwitchListTile( - selector: (context, s) => s.showThumbnailVideoDuration, - onChanged: (v) => settings.showThumbnailVideoDuration = v, - title: context.l10n.settingsThumbnailShowVideoDuration, - ), - ], - ); - } + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionThumbnails; + + @override + List tiles(BuildContext context) => [ + SettingsTileCollectionQuickActions(), + SettingsTileThumbnailOverlay(), + ]; +} + +class SettingsTileCollectionQuickActions extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsCollectionQuickActionsTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: CollectionActionEditorPage.routeName, + builder: (context) => const CollectionActionEditorPage(), + ); +} + +class SettingsTileThumbnailOverlay extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsThumbnailOverlayTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: ThumbnailOverlayPage.routeName, + builder: (context) => const ThumbnailOverlayPage(), + ); } diff --git a/lib/widgets/settings/video/controls.dart b/lib/widgets/settings/video/controls.dart index 1263508bb..17711bff5 100644 --- a/lib/widgets/settings/video/controls.dart +++ b/lib/widgets/settings/video/controls.dart @@ -5,30 +5,10 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:flutter/material.dart'; -class VideoControlsTile extends StatelessWidget { - const VideoControlsTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsVideoControlsTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: VideoControlsPage.routeName), - builder: (context) => const VideoControlsPage(), - ), - ); - }, - ); - } -} - class VideoControlsPage extends StatelessWidget { static const routeName = '/settings/video/controls'; - const VideoControlsPage({Key? key}) : super(key: key); + const VideoControlsPage({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/settings/video/subtitle_sample.dart b/lib/widgets/settings/video/subtitle_sample.dart index 208f9eea6..3ac584b26 100644 --- a/lib/widgets/settings/video/subtitle_sample.dart +++ b/lib/widgets/settings/video/subtitle_sample.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class SubtitleSample extends StatelessWidget { - const SubtitleSample({Key? key}) : super(key: key); + const SubtitleSample({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/settings/video/subtitle_theme.dart b/lib/widgets/settings/video/subtitle_theme.dart index 444c65456..436962e1d 100644 --- a/lib/widgets/settings/video/subtitle_theme.dart +++ b/lib/widgets/settings/video/subtitle_theme.dart @@ -7,30 +7,10 @@ import 'package:aves/widgets/settings/video/subtitle_sample.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class SubtitleThemeTile extends StatelessWidget { - const SubtitleThemeTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsSubtitleThemeTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: SubtitleThemePage.routeName), - builder: (context) => const SubtitleThemePage(), - ), - ); - }, - ); - } -} - class SubtitleThemePage extends StatelessWidget { static const routeName = '/settings/video/subtitle_theme'; - const SubtitleThemePage({Key? key}) : super(key: key); + const SubtitleThemePage({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/settings/video/video.dart b/lib/widgets/settings/video/video.dart index 4db331bc1..4e5636c5a 100644 --- a/lib/widgets/settings/video/video.dart +++ b/lib/widgets/settings/video/video.dart @@ -1,3 +1,5 @@ +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_loop_mode.dart'; @@ -5,100 +7,117 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; -import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:aves/widgets/settings/video/controls.dart'; import 'package:aves/widgets/settings/video/subtitle_theme.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class VideoSection extends StatelessWidget { - final ValueNotifier? expandedNotifier; +class VideoSection extends SettingsSection { final bool standalonePage; - const VideoSection({ - Key? key, - this.expandedNotifier, + VideoSection({ this.standalonePage = false, - }) : super(key: key); + }); @override - Widget build(BuildContext context) { - final children = [ - if (!standalonePage) - SettingsSwitchListTile( - selector: (context, s) => !s.hiddenFilters.contains(MimeFilter.video), - onChanged: (v) => settings.changeFilterVisibility({MimeFilter.video}, v), - title: context.l10n.settingsVideoShowVideos, - ), - SettingsSwitchListTile( + String get key => 'video'; + + @override + Widget icon(BuildContext context) => SettingsTileLeading( + icon: AIcons.video, + color: context.select((v) => v.video), + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionVideo; + + @override + FutureOr> tiles(BuildContext context) async { + return [ + if (!standalonePage) SettingsTileVideoShowVideos(), + SettingsTileVideoEnableHardwareAcceleration(), + SettingsTileVideoEnableAutoPlay(), + SettingsTileVideoLoopMode(), + SettingsTileVideoControls(), + SettingsTileVideoSubtitleTheme(), + ]; + } +} + +class SettingsTileVideoShowVideos extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsVideoShowVideos; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => !s.hiddenFilters.contains(MimeFilter.video), + onChanged: (v) => settings.changeFilterVisibility({MimeFilter.video}, v), + title: title(context), + ); +} + +class SettingsTileVideoEnableHardwareAcceleration extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsVideoEnableHardwareAcceleration; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( selector: (context, s) => s.enableVideoHardwareAcceleration, onChanged: (v) => settings.enableVideoHardwareAcceleration = v, - title: context.l10n.settingsVideoEnableHardwareAcceleration, - ), - SettingsSwitchListTile( + title: title(context), + ); +} + +class SettingsTileVideoEnableAutoPlay extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsVideoEnableAutoPlay; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( selector: (context, s) => s.enableVideoAutoPlay, onChanged: (v) => settings.enableVideoAutoPlay = v, - title: context.l10n.settingsVideoEnableAutoPlay, - ), - SettingsSelectionListTile( + title: title(context), + ); +} + +class SettingsTileVideoLoopMode extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsVideoLoopModeTile; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( values: VideoLoopMode.values, getName: (context, v) => v.getName(context), selector: (context, s) => s.videoLoopMode, onSelection: (v) => settings.videoLoopMode = v, - tileTitle: context.l10n.settingsVideoLoopModeTile, + tileTitle: title(context), dialogTitle: context.l10n.settingsVideoLoopModeTitle, - ), - const VideoControlsTile(), - const SubtitleThemeTile(), - ]; - - return standalonePage - ? ListView( - children: children, - ) - : AvesExpansionTile( - leading: SettingsTileLeading( - icon: AIcons.video, - color: context.select((v) => v.video), - ), - title: context.l10n.settingsSectionVideo, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: children, - ); - } + ); } -class VideoSettingsPage extends StatelessWidget { - static const routeName = '/settings/video'; - - const VideoSettingsPage({Key? key}) : super(key: key); +class SettingsTileVideoControls extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsVideoControlsTile; @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return MediaQueryDataProvider( - child: Scaffold( - appBar: AppBar( - title: Text(context.l10n.settingsVideoPageTitle), - ), - body: Theme( - data: theme.copyWith( - textTheme: theme.textTheme.copyWith( - // dense style font for tile subtitles, without modifying title font - bodyText2: const TextStyle(fontSize: 12), - ), - ), - child: const SafeArea( - child: VideoSection( - standalonePage: true, - ), - ), - ), - ), - ); - } + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: VideoControlsPage.routeName, + builder: (context) => const VideoControlsPage(), + ); +} + +class SettingsTileVideoSubtitleTheme extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsSubtitleThemeTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: SubtitleThemePage.routeName, + builder: (context) => const SubtitleThemePage(), + ); } diff --git a/lib/widgets/settings/video/video_settings_page.dart b/lib/widgets/settings/video/video_settings_page.dart new file mode 100644 index 000000000..4567144ec --- /dev/null +++ b/lib/widgets/settings/video/video_settings_page.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; +import 'package:aves/widgets/settings/video/video.dart'; +import 'package:flutter/material.dart'; + +class VideoSettingsPage extends StatefulWidget { + static const routeName = '/settings/video'; + + const VideoSettingsPage({super.key}); + + @override + State createState() => _VideoSettingsPageState(); +} + +class _VideoSettingsPageState extends State { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return MediaQueryDataProvider( + child: Scaffold( + appBar: AppBar( + title: Text(context.l10n.settingsVideoPageTitle), + ), + body: Theme( + data: theme.copyWith( + textTheme: theme.textTheme.copyWith( + // dense style font for tile subtitles, without modifying title font + bodyText2: const TextStyle(fontSize: 12), + ), + ), + child: SafeArea( + child: FutureBuilder>( + future: Future.value(VideoSection(standalonePage: true).tiles(context)), + builder: (context, snapshot) { + final tiles = snapshot.data; + if (tiles == null) return const SizedBox(); + + return ListView( + children: tiles.map((v) => v.build(context)).toList(), + ); + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/settings/viewer/entry_background.dart b/lib/widgets/settings/viewer/entry_background.dart index 4d808a33d..ab5efdf60 100644 --- a/lib/widgets/settings/viewer/entry_background.dart +++ b/lib/widgets/settings/viewer/entry_background.dart @@ -9,10 +9,10 @@ class EntryBackgroundSelector extends StatefulWidget { final ValueSetter setter; const EntryBackgroundSelector({ - Key? key, + super.key, required this.getter, required this.setter, - }) : super(key: key); + }); @override State createState() => _EntryBackgroundSelectorState(); diff --git a/lib/widgets/settings/viewer/overlay.dart b/lib/widgets/settings/viewer/overlay.dart index 463fa97e9..431050a89 100644 --- a/lib/widgets/settings/viewer/overlay.dart +++ b/lib/widgets/settings/viewer/overlay.dart @@ -5,30 +5,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; -class ViewerOverlayTile extends StatelessWidget { - const ViewerOverlayTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsViewerOverlayTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: ViewerOverlayPage.routeName), - builder: (context) => const ViewerOverlayPage(), - ), - ); - }, - ); - } -} - class ViewerOverlayPage extends StatelessWidget { static const routeName = '/settings/viewer_overlay'; - const ViewerOverlayPage({Key? key}) : super(key: key); + const ViewerOverlayPage({super.key}); @override Widget build(BuildContext context) { @@ -72,11 +52,6 @@ class ViewerOverlayPage extends StatelessWidget { onChanged: (v) => settings.showOverlayThumbnailPreview = v, title: context.l10n.settingsViewerShowOverlayThumbnails, ), - SettingsSwitchListTile( - selector: (context, s) => s.enableOverlayBlurEffect, - onChanged: (v) => settings.enableOverlayBlurEffect = v, - title: context.l10n.settingsViewerEnableOverlayBlurEffect, - ), ], ), ), diff --git a/lib/widgets/settings/viewer/viewer.dart b/lib/widgets/settings/viewer/viewer.dart index 5f993816a..35176bd92 100644 --- a/lib/widgets/settings/viewer/viewer.dart +++ b/lib/widgets/settings/viewer/viewer.dart @@ -1,95 +1,120 @@ +import 'dart:async'; + import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:aves/widgets/settings/viewer/entry_background.dart'; import 'package:aves/widgets/settings/viewer/overlay.dart'; import 'package:aves/widgets/settings/viewer/viewer_actions_editor.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class ViewerSection extends StatelessWidget { - final ValueNotifier expandedNotifier; - - const ViewerSection({ - Key? key, - required this.expandedNotifier, - }) : super(key: key); +class ViewerSection extends SettingsSection { + @override + String get key => 'viewer'; @override - Widget build(BuildContext context) { - return AvesExpansionTile( - leading: SettingsTileLeading( + Widget icon(BuildContext context) => SettingsTileLeading( icon: AIcons.image, color: context.select((v) => v.image), - ), - title: context.l10n.settingsSectionViewer, - expandedNotifier: expandedNotifier, - showHighlight: false, - children: [ - const ViewerActionsTile(), - const ViewerOverlayTile(), - const _CutoutModeSwitch(), - SettingsSwitchListTile( - selector: (context, s) => s.viewerMaxBrightness, - onChanged: (v) => settings.viewerMaxBrightness = v, - title: context.l10n.settingsViewerMaximumBrightness, - ), - SettingsSwitchListTile( - selector: (context, s) => s.enableMotionPhotoAutoPlay, - onChanged: (v) => settings.enableMotionPhotoAutoPlay = v, - title: context.l10n.settingsMotionPhotoAutoPlay, - ), - Selector( - selector: (context, s) => s.imageBackground, - builder: (context, current, child) => ListTile( - title: Text(context.l10n.settingsImageBackground), - trailing: EntryBackgroundSelector( - getter: () => current, - setter: (value) => settings.imageBackground = value, - ), + ); + + @override + String title(BuildContext context) => context.l10n.settingsSectionViewer; + + @override + FutureOr> tiles(BuildContext context) async { + final canSetCutoutMode = await windowService.canSetCutoutMode(); + return [ + SettingsTileViewerQuickActions(), + SettingsTileViewerOverlay(), + if (canSetCutoutMode) SettingsTileViewerCutoutMode(), + SettingsTileViewerMaxBrightness(), + SettingsTileViewerMotionPhotoAutoPlay(), + SettingsTileViewerImageBackground(), + ]; + } +} + +class SettingsTileViewerQuickActions extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsViewerQuickActionsTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: ViewerActionEditorPage.routeName, + builder: (context) => const ViewerActionEditorPage(), + ); +} + +class SettingsTileViewerOverlay extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsViewerOverlayTile; + + @override + Widget build(BuildContext context) => SettingsSubPageTile( + title: title(context), + routeName: ViewerOverlayPage.routeName, + builder: (context) => const ViewerOverlayPage(), + ); +} + +class SettingsTileViewerCutoutMode extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsViewerUseCutout; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.viewerUseCutout, + onChanged: (v) => settings.viewerUseCutout = v, + title: title(context), + ); +} + +class SettingsTileViewerMaxBrightness extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsViewerMaximumBrightness; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.viewerMaxBrightness, + onChanged: (v) => settings.viewerMaxBrightness = v, + title: title(context), + ); +} + +class SettingsTileViewerMotionPhotoAutoPlay extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsMotionPhotoAutoPlay; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.enableMotionPhotoAutoPlay, + onChanged: (v) => settings.enableMotionPhotoAutoPlay = v, + title: title(context), + ); +} + +class SettingsTileViewerImageBackground extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsImageBackground; + + @override + Widget build(BuildContext context) => Selector( + selector: (context, s) => s.imageBackground, + builder: (context, current, child) => ListTile( + title: Text(title(context)), + trailing: EntryBackgroundSelector( + getter: () => current, + setter: (value) => settings.imageBackground = value, ), ), - ], - ); - } -} - -class _CutoutModeSwitch extends StatefulWidget { - const _CutoutModeSwitch({Key? key}) : super(key: key); - - @override - State<_CutoutModeSwitch> createState() => _CutoutModeSwitchState(); -} - -class _CutoutModeSwitchState extends State<_CutoutModeSwitch> { - late Future _canSet; - - @override - void initState() { - super.initState(); - _canSet = windowService.canSetCutoutMode(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _canSet, - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!) { - return SettingsSwitchListTile( - selector: (context, s) => s.viewerUseCutout, - onChanged: (v) => settings.viewerUseCutout = v, - title: context.l10n.settingsViewerUseCutout, - ); - } - return const SizedBox.shrink(); - }, - ); - } + ); } diff --git a/lib/widgets/settings/viewer/viewer_actions_editor.dart b/lib/widgets/settings/viewer/viewer_actions_editor.dart index 453a58f96..795a2eddd 100644 --- a/lib/widgets/settings/viewer/viewer_actions_editor.dart +++ b/lib/widgets/settings/viewer/viewer_actions_editor.dart @@ -4,30 +4,10 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart'; import 'package:flutter/material.dart'; -class ViewerActionsTile extends StatelessWidget { - const ViewerActionsTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsViewerQuickActionsTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: ViewerActionEditorPage.routeName), - builder: (context) => const ViewerActionEditorPage(), - ), - ); - }, - ); - } -} - class ViewerActionEditorPage extends StatelessWidget { static const routeName = '/settings/viewer_actions'; - const ViewerActionEditorPage({Key? key}) : super(key: key); + const ViewerActionEditorPage({super.key}); static const allAvailableActions = [ EntryAction.share, diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart index 700b2d0c0..7a572a4eb 100644 --- a/lib/widgets/stats/filter_table.dart +++ b/lib/widgets/stats/filter_table.dart @@ -17,14 +17,14 @@ class FilterTable extends StatelessWidget { final FilterCallback onFilterSelection; const FilterTable({ - Key? key, + super.key, required this.totalEntryCount, required this.entryCountMap, required this.filterBuilder, required this.sortByCount, required this.maxRowCount, required this.onFilterSelection, - }) : super(key: key); + }); static const chipWidth = AvesFilterChip.defaultMaxChipWidth; static const countWidth = 32.0; @@ -72,35 +72,27 @@ class FilterTable extends StatelessWidget { ), ), if (showPercentIndicator) - // as of percent_indicator v4.0.0, bar radius is not correctly applied to progress bar - // when width is lower than height, so we clip it and handle padding outside - Padding( - padding: EdgeInsets.symmetric(horizontal: lineHeight), - child: ClipRRect( - borderRadius: BorderRadius.all(barRadius), - child: FutureBuilder( - future: filter.color(context), - builder: (context, snapshot) { - final color = snapshot.data; - return LinearPercentIndicator( - percent: percent, - lineHeight: lineHeight, - backgroundColor: theme.colorScheme.onPrimary.withOpacity(.1), - progressColor: isMonochrome ? theme.colorScheme.secondary : color, - animation: true, - isRTL: isRtl, - barRadius: barRadius, - center: Text( - intl.NumberFormat.percentPattern().format(percent), - style: TextStyle( - shadows: theme.brightness == Brightness.dark ? Constants.embossShadows : null, - ), - ), - padding: EdgeInsets.zero, - ); - }, - ), - ), + FutureBuilder( + future: filter.color(context), + builder: (context, snapshot) { + final color = snapshot.data; + return LinearPercentIndicator( + percent: percent, + lineHeight: lineHeight, + backgroundColor: theme.colorScheme.onPrimary.withOpacity(.1), + progressColor: isMonochrome ? theme.colorScheme.secondary : color, + animation: true, + isRTL: isRtl, + barRadius: barRadius, + center: Text( + intl.NumberFormat.percentPattern().format(percent), + style: TextStyle( + shadows: theme.brightness == Brightness.dark ? Constants.embossShadows : null, + ), + ), + padding: EdgeInsets.symmetric(horizontal: lineHeight), + ); + }, ), Text( '$count', diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index 24a594ca3..b6955616c 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -40,11 +40,11 @@ class StatsPage extends StatelessWidget { static const mimeDonutMinWidth = 124.0; StatsPage({ - Key? key, + super.key, required this.entries, required this.source, this.parentCollection, - }) : super(key: key) { + }) { entries.forEach((entry) { if (entry.hasAddress) { final address = entry.addressDetails!; @@ -110,36 +110,30 @@ class StatsPage extends StatelessWidget { padding: const EdgeInsets.all(16), child: Column( children: [ - // as of percent_indicator v4.0.0, bar radius is not correctly applied to progress bar - // when width is lower than height, so we clip it and handle padding outside Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(AIcons.location), - SizedBox(width: lineHeight), Expanded( - child: ClipRRect( - borderRadius: BorderRadius.all(barRadius), - child: LinearPercentIndicator( - percent: withGpsPercent, - lineHeight: lineHeight, - backgroundColor: theme.colorScheme.onPrimary.withOpacity(.1), - progressColor: theme.colorScheme.secondary, - animation: animate, - isRTL: context.isRtl, - barRadius: barRadius, - center: Text( - intl.NumberFormat.percentPattern().format(withGpsPercent), - style: TextStyle( - shadows: isDark ? Constants.embossShadows : null, - ), + child: LinearPercentIndicator( + percent: withGpsPercent, + lineHeight: lineHeight, + backgroundColor: theme.colorScheme.onPrimary.withOpacity(.1), + progressColor: theme.colorScheme.secondary, + animation: animate, + isRTL: context.isRtl, + barRadius: barRadius, + center: Text( + intl.NumberFormat.percentPattern().format(withGpsPercent), + style: TextStyle( + shadows: isDark ? Constants.embossShadows : null, ), - padding: EdgeInsets.zero, ), + padding: EdgeInsets.symmetric(horizontal: lineHeight), ), ), // end padding to match leading, so that inside label is aligned with outside label below - SizedBox(width: lineHeight + 24), + const SizedBox(width: 24), ], ), const SizedBox(height: 8), @@ -339,7 +333,7 @@ class StatsPage extends StatelessWidget { // we post closing the search page after applying the filter selection // so that hero animation target is ready in the `FilterBar`, // even when the target is a child of an `AnimatedList` - WidgetsBinding.instance!.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { Navigator.pop(context); }); } diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index 10decf912..b5b477cad 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -16,6 +16,7 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/enums.dart'; import 'package:aves/services/media/media_file_service.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/action_mixins/entry_storage.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; @@ -259,24 +260,26 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix source.refreshUris(newUris); final l10n = context.l10n; - final navigator = Navigator.of(context); final showAction = isMainMode && newUris.isNotEmpty ? SnackBarAction( label: l10n.showButtonLabel, onPressed: () { - // `context` may be obsolete if the user navigated away before triggering the action - // so we reused the navigator retrieved before showing the snack bar - navigator.pushAndRemoveUntil( - MaterialPageRoute( - settings: const RouteSettings(name: CollectionPage.routeName), - builder: (context) => CollectionPage( - source: source, - filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))}, - highlightTest: (entry) => newUris.contains(entry.uri), + // local context may be deactivated when action is triggered after navigation + final context = AvesApp.navigatorKey.currentContext; + if (context != null) { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + settings: const RouteSettings(name: CollectionPage.routeName), + builder: (context) => CollectionPage( + source: source, + filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))}, + highlightTest: (entry) => newUris.contains(entry.uri), + ), ), - ), - (route) => false, - ); + (route) => false, + ); + } }, ) : null; diff --git a/lib/widgets/viewer/debug/db.dart b/lib/widgets/viewer/debug/db.dart index 6575c1389..95b5d2fc7 100644 --- a/lib/widgets/viewer/debug/db.dart +++ b/lib/widgets/viewer/debug/db.dart @@ -12,9 +12,9 @@ class DbTab extends StatefulWidget { final AvesEntry entry; const DbTab({ - Key? key, + super.key, required this.entry, - }) : super(key: key); + }); @override State createState() => _DbTabState(); diff --git a/lib/widgets/viewer/debug/debug_page.dart b/lib/widgets/viewer/debug/debug_page.dart index 2fdd44e23..a6468fd81 100644 --- a/lib/widgets/viewer/debug/debug_page.dart +++ b/lib/widgets/viewer/debug/debug_page.dart @@ -16,9 +16,9 @@ class ViewerDebugPage extends StatelessWidget { final AvesEntry entry; const ViewerDebugPage({ - Key? key, + super.key, required this.entry, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/viewer/debug/metadata.dart b/lib/widgets/viewer/debug/metadata.dart index 45c525d02..e75435a61 100644 --- a/lib/widgets/viewer/debug/metadata.dart +++ b/lib/widgets/viewer/debug/metadata.dart @@ -13,9 +13,9 @@ class MetadataTab extends StatefulWidget { final AvesEntry entry; const MetadataTab({ - Key? key, + super.key, required this.entry, - }) : super(key: key); + }); @override State createState() => _MetadataTabState(); diff --git a/lib/widgets/viewer/embedded/embedded_data_opener.dart b/lib/widgets/viewer/embedded/embedded_data_opener.dart index 76e93359c..3152d7461 100644 --- a/lib/widgets/viewer/embedded/embedded_data_opener.dart +++ b/lib/widgets/viewer/embedded/embedded_data_opener.dart @@ -16,10 +16,10 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin { final Widget child; const EmbeddedDataOpener({ - Key? key, + super.key, required this.entry, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/viewer/entry_horizontal_pager.dart b/lib/widgets/viewer/entry_horizontal_pager.dart index a2dc6463d..1c27b0c03 100644 --- a/lib/widgets/viewer/entry_horizontal_pager.dart +++ b/lib/widgets/viewer/entry_horizontal_pager.dart @@ -7,6 +7,7 @@ import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -17,12 +18,12 @@ class MultiEntryScroller extends StatefulWidget { final void Function(AvesEntry mainEntry, AvesEntry? pageEntry) onViewDisposed; const MultiEntryScroller({ - Key? key, + super.key, required this.collection, required this.pageController, required this.onPageChanged, required this.onViewDisposed, - }) : super(key: key); + }); @override State createState() => _MultiEntryScrollerState(); @@ -44,7 +45,10 @@ class _MultiEntryScrollerState extends State with AutomaticK key: const Key('horizontal-pageview'), scrollDirection: Axis.horizontal, controller: pageController, - physics: const MagnifierScrollerPhysics(parent: BouncingScrollPhysics()), + physics: MagnifierScrollerPhysics( + gestureSettings: context.select((mq) => mq.gestureSettings), + parent: const BouncingScrollPhysics(), + ), onPageChanged: widget.onPageChanged, itemBuilder: (context, index) { final mainEntry = entries[index]; @@ -108,9 +112,9 @@ class SingleEntryScroller extends StatefulWidget { final AvesEntry entry; const SingleEntryScroller({ - Key? key, + super.key, required this.entry, - }) : super(key: key); + }); @override State createState() => _SingleEntryScrollerState(); diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index 247de072a..ed08f48ae 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -10,8 +10,10 @@ import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart'; import 'package:aves/widgets/viewer/entry_horizontal_pager.dart'; import 'package:aves/widgets/viewer/info/info_page.dart'; import 'package:aves/widgets/viewer/notifications.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; import 'package:screen_brightness/screen_brightness.dart'; class ViewerVerticalPageView extends StatefulWidget { @@ -23,7 +25,7 @@ class ViewerVerticalPageView extends StatefulWidget { final void Function(AvesEntry mainEntry, AvesEntry? pageEntry) onViewDisposed; const ViewerVerticalPageView({ - Key? key, + super.key, required this.collection, required this.entryNotifier, required this.verticalPager, @@ -32,7 +34,7 @@ class ViewerVerticalPageView extends StatefulWidget { required this.onHorizontalPageChanged, required this.onImagePageRequested, required this.onViewDisposed, - }) : super(key: key); + }); @override State createState() => _ViewerVerticalPageViewState(); @@ -136,7 +138,10 @@ class _ViewerVerticalPageViewState extends State { key: const Key('vertical-pageview'), scrollDirection: Axis.vertical, controller: widget.verticalPager, - physics: const MagnifierScrollerPhysics(parent: PageScrollPhysics()), + physics: MagnifierScrollerPhysics( + gestureSettings: context.select((mq) => mq.gestureSettings), + parent: const PageScrollPhysics(), + ), onPageChanged: widget.onVerticalPageChanged, children: pages, ), diff --git a/lib/widgets/viewer/entry_viewer_page.dart b/lib/widgets/viewer/entry_viewer_page.dart index 8caf6e5cd..814e1b703 100644 --- a/lib/widgets/viewer/entry_viewer_page.dart +++ b/lib/widgets/viewer/entry_viewer_page.dart @@ -17,10 +17,10 @@ class EntryViewerPage extends StatelessWidget { final AvesEntry initialEntry; const EntryViewerPage({ - Key? key, + super.key, this.collection, required this.initialEntry, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -55,9 +55,9 @@ class ViewStateConductorProvider extends StatelessWidget { final Widget? child; const ViewStateConductorProvider({ - Key? key, + super.key, this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -77,9 +77,9 @@ class VideoConductorProvider extends StatelessWidget { final Widget? child; const VideoConductorProvider({ - Key? key, + super.key, this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -97,9 +97,9 @@ class MultiPageConductorProvider extends StatelessWidget { final Widget? child; const MultiPageConductorProvider({ - Key? key, + super.key, this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 4d7fbb395..a6fdf3aa1 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -44,10 +44,10 @@ class EntryViewerStack extends StatefulWidget { final AvesEntry initialEntry; const EntryViewerStack({ - Key? key, + super.key, this.collection, required this.initialEntry, - }) : super(key: key); + }); @override State createState() => _EntryViewerStackState(); @@ -132,8 +132,8 @@ class _EntryViewerStackState extends State with FeedbackMixin, ); _initEntryControllers(entry); _registerWidget(widget); - WidgetsBinding.instance!.addObserver(this); - WidgetsBinding.instance!.addPostFrameCallback((_) => _initOverlay()); + WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance.addPostFrameCallback((_) => _initOverlay()); } @override @@ -150,7 +150,7 @@ class _EntryViewerStackState extends State with FeedbackMixin, _overlayAnimationController.dispose(); _overlayVisible.removeListener(_onOverlayVisibleChange); _verticalPager.removeListener(_onVerticalPageControllerChange); - WidgetsBinding.instance!.removeObserver(this); + WidgetsBinding.instance.removeObserver(this); _unregisterWidget(widget); super.dispose(); } @@ -395,7 +395,7 @@ class _EntryViewerStackState extends State with FeedbackMixin, builder: (context, mqHeight, child) { // when orientation change, the `PageController` offset is not updated right away // and it does not trigger its listeners when it does, so we force a refresh in the next frame - WidgetsBinding.instance!.addPostFrameCallback((_) => _onVerticalPageControllerChange()); + WidgetsBinding.instance.addPostFrameCallback((_) => _onVerticalPageControllerChange()); return AnimatedBuilder( animation: _verticalScrollNotifier, builder: (context, child) => Positioned( @@ -548,7 +548,7 @@ class _EntryViewerStackState extends State with FeedbackMixin, if (_heroInfoNotifier.value != heroInfo) { _heroInfoNotifier.value = heroInfo; // we post closing the viewer page so that hero animation source is ready - WidgetsBinding.instance!.addPostFrameCallback((_) => pop()); + WidgetsBinding.instance.addPostFrameCallback((_) => pop()); } else { // viewer already has correct hero info, no need to rebuild pop(); @@ -683,7 +683,7 @@ class _EntryViewerStackState extends State with FeedbackMixin, if (videoPageEntries.isNotEmpty) { // init video controllers for all pages that could need it final videoConductor = context.read(); - videoPageEntries.forEach(videoConductor.getOrCreateController); + videoPageEntries.forEach((entry) => videoConductor.getOrCreateController(entry, maxControllerCount: videoPageEntries.length)); // auto play/pause when changing page Future _onPageChange() async { @@ -693,8 +693,11 @@ class _EntryViewerStackState extends State with FeedbackMixin, final pageInfo = multiPageInfo.getByIndex(page)!; if (pageInfo.isVideo) { final pageEntry = multiPageInfo.getPageEntryByIndex(page); - final pageVideoController = videoConductor.getController(pageEntry)!; - await _playVideo(pageVideoController, () => entry == _entryNotifier.value && page == multiPageController.page); + final pageVideoController = videoConductor.getController(pageEntry); + assert(pageVideoController != null); + if (pageVideoController != null) { + await _playVideo(pageVideoController, () => entry == _entryNotifier.value && page == multiPageController.page); + } } } } diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index 6027073fb..a7c11a944 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -1,3 +1,5 @@ +import 'package:aves/app_mode.dart'; +import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/model/actions/entry_info_actions.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; @@ -7,16 +9,19 @@ import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/type.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/services/common/services.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/format.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart'; import 'package:aves/widgets/viewer/info/common.dart'; -import 'package:aves/widgets/viewer/info/owner.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -28,54 +33,23 @@ class BasicSection extends StatelessWidget { final FilterCallback onFilter; const BasicSection({ - Key? key, + super.key, required this.entry, this.collection, required this.actionDelegate, required this.isEditingMetadataNotifier, required this.onFilter, - }) : super(key: key); - - int get megaPixels => entry.megaPixels; - - bool get showMegaPixels => entry.isPhoto && megaPixels > 0; - - String get rasterResolutionText => '${entry.resolutionText}${showMegaPixels ? ' • $megaPixels MP' : ''}'; + }); @override Widget build(BuildContext context) { - final l10n = context.l10n; - final infoUnknown = l10n.viewerInfoUnknown; - final locale = l10n.localeName; - final use24hour = context.select((v) => v.alwaysUse24HourFormat); - return AnimatedBuilder( animation: entry.metadataChangeNotifier, builder: (context, child) { - // TODO TLAD line break on all characters for the following fields when this is fixed: https://github.com/flutter/flutter/issues/61081 - // inserting ZWSP (\u200B) between characters does help, but it messes with width and height computation (another Flutter issue) - final title = entry.bestTitle ?? infoUnknown; - final date = entry.bestDate; - final dateText = date != null ? formatDateTime(date, locale, use24hour) : infoUnknown; - final showResolution = !entry.isSvg && entry.isSized; - final sizeText = entry.sizeBytes != null ? formatFileSize(locale, entry.sizeBytes!) : infoUnknown; - final path = entry.path; - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - InfoRowGroup( - info: { - l10n.viewerInfoLabelTitle: title, - l10n.viewerInfoLabelDate: dateText, - if (entry.isVideo) ..._buildVideoRows(context), - if (showResolution) l10n.viewerInfoLabelResolution: rasterResolutionText, - l10n.viewerInfoLabelSize: sizeText, - if (!entry.trashed) l10n.viewerInfoLabelUri: entry.uri, - if (path != null) l10n.viewerInfoLabelPath: path, - }, - ), - if (!entry.trashed) OwnerProp(entry: entry), + _BasicInfo(entry: entry), _buildChips(context), _buildEditButtons(context), ], @@ -184,10 +158,130 @@ class BasicSection extends StatelessWidget { }, ); } +} + +class _BasicInfo extends StatefulWidget { + final AvesEntry entry; + + const _BasicInfo({ + required this.entry, + }); + + @override + State<_BasicInfo> createState() => _BasicInfoState(); +} + +class _BasicInfoState extends State<_BasicInfo> { + Future _ownerPackageLoader = SynchronousFuture(null); + Future _appNameLoader = SynchronousFuture(null); + + AvesEntry get entry => widget.entry; + + int get megaPixels => entry.megaPixels; + + bool get showMegaPixels => entry.isPhoto && megaPixels > 0; + + String get rasterResolutionText => '${entry.resolutionText}${showMegaPixels ? ' • $megaPixels MP' : ''}'; + + static const ownerPackageNamePropKey = 'owner_package_name'; + static const iconSize = 20.0; + + @override + void initState() { + super.initState(); + if (!entry.trashed) { + final isMediaContent = entry.uri.startsWith('content://media/external/'); + if (isMediaContent) { + _ownerPackageLoader = metadataFetchService.hasContentResolverProp(ownerPackageNamePropKey).then((exists) { + return exists ? metadataFetchService.getContentResolverProp(entry, ownerPackageNamePropKey) : SynchronousFuture(null); + }); + final isViewerMode = context.read>().value == AppMode.view; + if (isViewerMode && settings.isInstalledAppAccessAllowed) { + _appNameLoader = androidFileUtils.initAppNames(); + } + } + } + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final infoUnknown = l10n.viewerInfoUnknown; + final locale = l10n.localeName; + final use24hour = context.select((v) => v.alwaysUse24HourFormat); + + // TODO TLAD line break on all characters for the following fields when this is fixed: https://github.com/flutter/flutter/issues/61081 + // inserting ZWSP (\u200B) between characters does help, but it messes with width and height computation (another Flutter issue) + final title = entry.bestTitle ?? infoUnknown; + final date = entry.bestDate; + final dateText = date != null ? formatDateTime(date, locale, use24hour) : infoUnknown; + final showResolution = !entry.isSvg && entry.isSized; + final sizeText = entry.sizeBytes != null ? formatFileSize(locale, entry.sizeBytes!) : infoUnknown; + final path = entry.path; + + return FutureBuilder( + future: _ownerPackageLoader, + builder: (context, snapshot) { + final ownerPackage = snapshot.data; + return FutureBuilder( + future: _appNameLoader, + builder: (context, snapshot) { + return InfoRowGroup( + info: { + l10n.viewerInfoLabelTitle: title, + l10n.viewerInfoLabelDate: dateText, + if (entry.isVideo) ..._buildVideoRows(context), + if (showResolution) l10n.viewerInfoLabelResolution: rasterResolutionText, + l10n.viewerInfoLabelSize: sizeText, + if (!entry.trashed) l10n.viewerInfoLabelUri: entry.uri, + if (path != null) l10n.viewerInfoLabelPath: path, + if (ownerPackage != null) l10n.viewerInfoLabelOwner: ownerPackage, + }, + spanBuilders: { + l10n.viewerInfoLabelOwner: _ownerHandler(ownerPackage), + }, + ); + }, + ); + }, + ); + } Map _buildVideoRows(BuildContext context) { return { context.l10n.viewerInfoLabelDuration: entry.durationText, }; } + + InfoValueSpanBuilder _ownerHandler(String? ownerPackage) { + if (ownerPackage == null) return (context, key, value) => []; + + final appName = androidFileUtils.getCurrentAppName(ownerPackage) ?? ownerPackage; + return (context, key, value) => [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: const EdgeInsetsDirectional.only(end: 4), + child: ConstrainedBox( + // use constraints instead of sizing `Image`, + // so that it can collapse when handling an empty image + constraints: const BoxConstraints( + maxWidth: iconSize, + maxHeight: iconSize, + ), + child: Image( + image: AppIconImage( + packageName: ownerPackage, + size: iconSize, + ), + ), + ), + ), + ), + TextSpan( + text: appName, + style: InfoRowGroup.valueStyle, + ), + ]; + } } diff --git a/lib/widgets/viewer/info/common.dart b/lib/widgets/viewer/info/common.dart index f6e72a77d..134dda62b 100644 --- a/lib/widgets/viewer/info/common.dart +++ b/lib/widgets/viewer/info/common.dart @@ -10,9 +10,9 @@ class SectionRow extends StatelessWidget { final IconData icon; const SectionRow({ - Key? key, + super.key, required this.icon, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -43,7 +43,7 @@ class SectionRow extends StatelessWidget { class InfoRowGroup extends StatefulWidget { final Map info; final int maxValueLength; - final Map? linkHandlers; + final Map spanBuilders; static const keyValuePadding = 16; static const fontSize = 13.0; @@ -53,14 +53,31 @@ class InfoRowGroup extends StatefulWidget { static TextStyle keyStyle(BuildContext context) => Theme.of(context).textTheme.caption!.merge(_keyStyle); const InfoRowGroup({ - Key? key, + super.key, required this.info, this.maxValueLength = 0, - this.linkHandlers, - }) : super(key: key); + Map? spanBuilders, + }) : spanBuilders = spanBuilders ?? const {}; @override State createState() => _InfoRowGroupState(); + + static InfoValueSpanBuilder linkSpanBuilder({ + required String Function(BuildContext context) linkText, + required void Function(BuildContext context) onTap, + }) { + return (context, key, value) { + value = linkText(context); + // open link on tap + final recognizer = TapGestureRecognizer()..onTap = () => onTap(context); + // `colorScheme.secondary` is overridden upstream as an `ExpansionTileCard` theming workaround, + // so we use `colorScheme.primary` instead + final linkColor = Theme.of(context).colorScheme.primary; + final style = InfoRowGroup.valueStyle.copyWith(color: linkColor, decoration: TextDecoration.underline); + + return [TextSpan(text: '${Constants.fsi}$value${Constants.pdi}', style: style, recognizer: recognizer)]; + }; + } } class _InfoRowGroupState extends State { @@ -70,7 +87,7 @@ class _InfoRowGroupState extends State { int get maxValueLength => widget.maxValueLength; - Map? get linkHandlers => widget.linkHandlers; + Map get spanBuilders => widget.spanBuilders; @override Widget build(BuildContext context) { @@ -94,34 +111,8 @@ class _InfoRowGroupState extends State { children: keyValues.entries.expand( (kv) { final key = kv.key; - String value; - TextStyle? style; - GestureRecognizer? recognizer; - - if (linkHandlers?.containsKey(key) == true) { - final handler = linkHandlers![key]!; - value = handler.linkText(context); - // open link on tap - recognizer = TapGestureRecognizer()..onTap = () => handler.onTap(context); - // `colorScheme.secondary` is overridden upstream as an `ExpansionTileCard` theming workaround, - // so we use `colorScheme.primary` instead - final linkColor = Theme.of(context).colorScheme.primary; - style = InfoRowGroup.valueStyle.copyWith(color: linkColor, decoration: TextDecoration.underline); - } else { - value = kv.value; - // long values are clipped, and made expandable by tapping them - final showPreviewOnly = maxValueLength > 0 && value.length > maxValueLength && !_expandedKeys.contains(key); - if (showPreviewOnly) { - value = '${value.substring(0, maxValueLength)}…'; - // show full value on tap - recognizer = TapGestureRecognizer()..onTap = () => setState(() => _expandedKeys.add(key)); - } - } - - if (key != lastKey) { - value = '$value\n'; - } - + final value = kv.value; + final spanBuilder = spanBuilders[key] ?? _buildTextValueSpans; final thisSpaceSize = max(0.0, (baseValueX - keySizes[key]!)) + InfoRowGroup.keyValuePadding; // each text span embeds and pops a Bidi isolate, @@ -130,8 +121,16 @@ class _InfoRowGroupState extends State { // and each span respects the directionality of its inner text only return [ TextSpan(text: '${Constants.fsi}$key${Constants.pdi}', style: _keyStyle), - WidgetSpan(child: SizedBox(width: thisSpaceSize)), - TextSpan(text: '${Constants.fsi}$value${Constants.pdi}', style: style, recognizer: recognizer), + WidgetSpan( + child: SizedBox( + width: thisSpaceSize, + // as of Flutter v3.0.0, the underline decoration from the following `TextSpan` + // is applied to the `WidgetSpan` too, so we add a dummy `Text` as a workaround + child: const Text(''), + ), + ), + ...spanBuilder(context, key, value), + if (key != lastKey) const TextSpan(text: '\n'), ]; }, ).toList(), @@ -150,14 +149,20 @@ class _InfoRowGroupState extends State { )..layout(const BoxConstraints(), parentUsesSize: true); return para.getMaxIntrinsicWidth(double.infinity); } + + List _buildTextValueSpans(BuildContext context, String key, String value) { + GestureRecognizer? recognizer; + + // long values are clipped, and made expandable by tapping them + final showPreviewOnly = maxValueLength > 0 && value.length > maxValueLength && !_expandedKeys.contains(key); + if (showPreviewOnly) { + value = '${value.substring(0, maxValueLength)}…'; + // show full value on tap + recognizer = TapGestureRecognizer()..onTap = () => setState(() => _expandedKeys.add(key)); + } + + return [TextSpan(text: '${Constants.fsi}$value${Constants.pdi}', recognizer: recognizer)]; + } } -class InfoLinkHandler { - final String Function(BuildContext context) linkText; - final void Function(BuildContext context) onTap; - - const InfoLinkHandler({ - required this.linkText, - required this.onTap, - }); -} +typedef InfoValueSpanBuilder = List Function(BuildContext context, String key, String value); diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart index 761d99ae9..7d3fb8ea0 100644 --- a/lib/widgets/viewer/info/info_app_bar.dart +++ b/lib/widgets/viewer/info/info_app_bar.dart @@ -20,12 +20,12 @@ class InfoAppBar extends StatelessWidget { final VoidCallback onBackPressed; const InfoAppBar({ - Key? key, + super.key, required this.entry, required this.actionDelegate, required this.metadataNotifier, required this.onBackPressed, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -74,7 +74,6 @@ class InfoAppBar extends StatelessWidget { ), ), ], - titleSpacing: 0, floating: true, ); } diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 67e0b0b6c..f2a2a5a21 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -26,11 +26,11 @@ class InfoPage extends StatefulWidget { final ValueNotifier isScrollingNotifier; const InfoPage({ - Key? key, + super.key, required this.collection, required this.entryNotifier, required this.isScrollingNotifier, - }) : super(key: key); + }); @override State createState() => _InfoPageState(); @@ -132,14 +132,13 @@ class _InfoPageContent extends StatefulWidget { final VoidCallback goToViewer; const _InfoPageContent({ - Key? key, required this.collection, required this.entry, required this.isScrollingNotifier, required this.scrollController, required this.split, required this.goToViewer, - }) : super(key: key); + }); @override State<_InfoPageContent> createState() => _InfoPageContentState(); diff --git a/lib/widgets/viewer/info/info_search.dart b/lib/widgets/viewer/info/info_search.dart index d40680cd4..6dbb9200a 100644 --- a/lib/widgets/viewer/info/info_search.dart +++ b/lib/widgets/viewer/info/info_search.dart @@ -1,6 +1,5 @@ import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/common/animated_icons_fix.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; @@ -27,9 +26,8 @@ class InfoSearchDelegate extends SearchDelegate { @override Widget buildLeading(BuildContext context) { return IconButton( - // TODO TLAD [rtl] replace to regular `AnimatedIcon` when this is fixed: https://github.com/flutter/flutter/issues/60521 - icon: AnimatedIconFixIssue60521( - icon: AnimatedIconsFixIssue60521.menu_arrow, + icon: AnimatedIcon( + icon: AnimatedIcons.menu_arrow, progress: transitionAnimation, ), onPressed: () => Navigator.pop(context), diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index b71e1561f..ed18d17ba 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -7,11 +7,11 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/theme/icons.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/map/controller.dart'; import 'package:aves/widgets/common/map/geo_map.dart'; -import 'package:aves/widgets/common/map/theme.dart'; +import 'package:aves/widgets/common/providers/map_theme_provider.dart'; import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/viewer/info/common.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; class LocationSection extends StatefulWidget { @@ -22,13 +22,13 @@ class LocationSection extends StatefulWidget { final FilterCallback onFilter; const LocationSection({ - Key? key, + super.key, required this.collection, required this.entry, required this.showTitle, required this.isScrollingNotifier, required this.onFilter, - }) : super(key: key); + }); @override State createState() => _LocationSectionState(); diff --git a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart index 308c581a5..51b1502e8 100644 --- a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart +++ b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart @@ -26,14 +26,14 @@ class MetadataDirTile extends StatelessWidget { final bool initiallyExpanded, showThumbnails; const MetadataDirTile({ - Key? key, + super.key, required this.entry, required this.title, required this.dir, this.expandedDirectoryNotifier, this.initiallyExpanded = false, this.showThumbnails = true, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -50,7 +50,7 @@ class MetadataDirTile extends StatelessWidget { initiallyExpanded: initiallyExpanded, ); } else { - Map? linkHandlers; + Map? linkHandlers; switch (dirName) { case SvgMetadataService.metadataDirectory: linkHandlers = getSvgLinkHandlers(tags); @@ -79,7 +79,7 @@ class MetadataDirTile extends StatelessWidget { child: InfoRowGroup( info: tags, maxValueLength: Constants.infoGroupMaxValueLength, - linkHandlers: linkHandlers, + spanBuilders: linkHandlers, ), ), ], @@ -87,9 +87,9 @@ class MetadataDirTile extends StatelessWidget { } } - static Map getSvgLinkHandlers(SplayTreeMap tags) { + static Map getSvgLinkHandlers(SplayTreeMap tags) { return { - 'Metadata': InfoLinkHandler( + 'Metadata': InfoRowGroup.linkSpanBuilder( linkText: (context) => context.l10n.viewerInfoViewXmlLinkText, onTap: (context) { Navigator.push( @@ -106,9 +106,9 @@ class MetadataDirTile extends StatelessWidget { }; } - static Map getVideoCoverLinkHandlers(SplayTreeMap tags) { + static Map getVideoCoverLinkHandlers(SplayTreeMap tags) { return { - 'Image': InfoLinkHandler( + 'Image': InfoRowGroup.linkSpanBuilder( linkText: (context) => context.l10n.viewerInfoOpenLinkText, onTap: (context) => OpenEmbeddedDataNotification.videoCover().dispatch(context), ), diff --git a/lib/widgets/viewer/info/metadata/metadata_section.dart b/lib/widgets/viewer/info/metadata/metadata_section.dart index fe003688e..ecd0efa25 100644 --- a/lib/widgets/viewer/info/metadata/metadata_section.dart +++ b/lib/widgets/viewer/info/metadata/metadata_section.dart @@ -23,10 +23,10 @@ class MetadataSectionSliver extends StatefulWidget { final ValueNotifier> metadataNotifier; const MetadataSectionSliver({ - Key? key, + super.key, required this.entry, required this.metadataNotifier, - }) : super(key: key); + }); @override State createState() => _MetadataSectionSliverState(); diff --git a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart index 477f7ec34..0bb79ee82 100644 --- a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart +++ b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart @@ -10,9 +10,9 @@ class MetadataThumbnails extends StatefulWidget { final AvesEntry entry; const MetadataThumbnails({ - Key? key, + super.key, required this.entry, - }) : super(key: key); + }); @override State createState() => _MetadataThumbnailsState(); diff --git a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart index 9752f011f..8a25664cc 100644 --- a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart +++ b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart @@ -145,7 +145,7 @@ class XmpNamespace extends Equatable { InfoRowGroup( info: Map.fromEntries(props.map((prop) => MapEntry(prop.displayKey, formatValue(prop)))), maxValueLength: Constants.infoGroupMaxValueLength, - linkHandlers: linkifyValues(props), + spanBuilders: linkifyValues(props), ), ...buildFromExtractedData(), ]; @@ -194,7 +194,7 @@ class XmpNamespace extends Equatable { String formatValue(XmpProp prop) => prop.value; - Map linkifyValues(List props) => {}; + Map linkifyValues(List props) => {}; } class XmpProp { diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart index 8081111c0..950241428 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart @@ -13,7 +13,7 @@ abstract class XmpGoogleNamespace extends XmpNamespace { List> get dataProps; @override - Map linkifyValues(List props) { + Map linkifyValues(List props) { return Map.fromEntries(dataProps.map((t) { final dataPropPath = t.item1; final mimePropPath = t.item2; @@ -22,7 +22,7 @@ abstract class XmpGoogleNamespace extends XmpNamespace { return (dataProp != null && mimeProp != null) ? MapEntry( dataProp.displayKey, - InfoLinkHandler( + InfoRowGroup.linkSpanBuilder( linkText: (context) => context.l10n.viewerInfoOpenLinkText, onTap: (context) => OpenEmbeddedDataNotification.xmp( propPath: dataProp.path, diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart index 2d41cc381..2c4e0fe65 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart @@ -29,7 +29,7 @@ class XmpBasicNamespace extends XmpNamespace { final struct = thumbnails[index]!; return { if (struct.containsKey(thumbnailDataDisplayKey)) - thumbnailDataDisplayKey: InfoLinkHandler( + thumbnailDataDisplayKey: InfoRowGroup.linkSpanBuilder( linkText: (context) => context.l10n.viewerInfoOpenLinkText, onTap: (context) => OpenEmbeddedDataNotification.xmp( propPath: 'xmp:Thumbnails[$index]/xmpGImg:image', diff --git a/lib/widgets/viewer/info/metadata/xmp_structs.dart b/lib/widgets/viewer/info/metadata/xmp_structs.dart index 372d9d2a7..88f66e790 100644 --- a/lib/widgets/viewer/info/metadata/xmp_structs.dart +++ b/lib/widgets/viewer/info/metadata/xmp_structs.dart @@ -13,14 +13,14 @@ import 'package:flutter/material.dart'; class XmpStructArrayCard extends StatefulWidget { final String title; late final List> structs; - final Map Function(int index)? linkifier; + final Map Function(int index)? linkifier; XmpStructArrayCard({ - Key? key, + super.key, required this.title, required Map> structByIndex, this.linkifier, - }) : super(key: key) { + }) { final length = structByIndex.keys.fold(0, max); structs = [for (var i = 0; i < length; i++) structByIndex[i + 1] ?? {}]; } @@ -95,7 +95,7 @@ class _XmpStructArrayCardState extends State { child: InfoRowGroup( info: structs[_index], maxValueLength: Constants.infoGroupMaxValueLength, - linkHandlers: widget.linkifier?.call(_index + 1), + spanBuilders: widget.linkifier?.call(_index + 1), ), ), ), @@ -108,16 +108,16 @@ class _XmpStructArrayCardState extends State { class XmpStructCard extends StatelessWidget { final String title; final Map struct; - final Map Function()? linkifier; + final Map Function()? linkifier; static const cardMargin = EdgeInsets.symmetric(vertical: 8, horizontal: 0); const XmpStructCard({ - Key? key, + super.key, required this.title, required this.struct, this.linkifier, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -137,7 +137,7 @@ class XmpStructCard extends StatelessWidget { InfoRowGroup( info: struct, maxValueLength: Constants.infoGroupMaxValueLength, - linkHandlers: linkifier?.call(), + spanBuilders: linkifier?.call(), ), ], ), diff --git a/lib/widgets/viewer/info/metadata/xmp_tile.dart b/lib/widgets/viewer/info/metadata/xmp_tile.dart index aa7d2c79d..dcacff9a7 100644 --- a/lib/widgets/viewer/info/metadata/xmp_tile.dart +++ b/lib/widgets/viewer/info/metadata/xmp_tile.dart @@ -17,13 +17,13 @@ class XmpDirTile extends StatefulWidget { final bool initiallyExpanded; const XmpDirTile({ - Key? key, + super.key, required this.entry, required this.title, required this.tags, required this.expandedNotifier, required this.initiallyExpanded, - }) : super(key: key); + }); @override State createState() => _XmpDirTileState(); diff --git a/lib/widgets/viewer/info/owner.dart b/lib/widgets/viewer/info/owner.dart deleted file mode 100644 index 302516c51..000000000 --- a/lib/widgets/viewer/info/owner.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:aves/app_mode.dart'; -import 'package:aves/image_providers/app_icon_image_provider.dart'; -import 'package:aves/model/entry.dart'; -import 'package:aves/model/settings/settings.dart'; -import 'package:aves/services/common/services.dart'; -import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/viewer/info/common.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class OwnerProp extends StatefulWidget { - final AvesEntry entry; - - const OwnerProp({ - Key? key, - required this.entry, - }) : super(key: key); - - @override - State createState() => _OwnerPropState(); -} - -class _OwnerPropState extends State { - Future _ownerPackageLoader = SynchronousFuture(null); - Future _appNameLoader = SynchronousFuture(null); - - AvesEntry get entry => widget.entry; - - static const ownerPackageNamePropKey = 'owner_package_name'; - static const iconSize = 20.0; - - @override - void initState() { - super.initState(); - final isMediaContent = entry.uri.startsWith('content://media/external/'); - if (isMediaContent) { - _ownerPackageLoader = metadataFetchService.hasContentResolverProp(ownerPackageNamePropKey).then((exists) { - return exists ? metadataFetchService.getContentResolverProp(entry, ownerPackageNamePropKey) : SynchronousFuture(null); - }); - final isViewerMode = context.read>().value == AppMode.view; - if (isViewerMode && settings.isInstalledAppAccessAllowed) { - _appNameLoader = androidFileUtils.initAppNames(); - } - } - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _ownerPackageLoader, - builder: (context, snapshot) { - final ownerPackage = snapshot.data; - if (ownerPackage == null) return const SizedBox(); - - return FutureBuilder( - future: _appNameLoader, - builder: (context, snapshot) { - final appName = androidFileUtils.getCurrentAppName(ownerPackage) ?? ownerPackage; - return SelectableText.rich( - TextSpan( - children: [ - TextSpan( - text: context.l10n.viewerInfoLabelOwner, - style: InfoRowGroup.keyStyle(context), - ), - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Image( - image: AppIconImage( - packageName: ownerPackage, - size: iconSize, - ), - width: iconSize, - height: iconSize, - ), - ), - ), - TextSpan( - text: appName, - style: InfoRowGroup.valueStyle, - ), - ], - ), - ); - }, - ); - }, - ); - } -} diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index 39d016c8e..8c72175a2 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -21,7 +21,7 @@ class ViewerBottomOverlay extends StatefulWidget { final MultiPageController? multiPageController; const ViewerBottomOverlay({ - Key? key, + super.key, required this.entries, required this.index, required this.hasCollection, @@ -29,7 +29,7 @@ class ViewerBottomOverlay extends StatefulWidget { this.viewInsets, this.viewPadding, required this.multiPageController, - }) : super(key: key); + }); @override State createState() => _ViewerBottomOverlayState(); @@ -60,6 +60,8 @@ class _ViewerBottomOverlayState extends State { mainEntry: mainEntry, pageEntry: pageEntry ?? mainEntry, hasCollection: widget.hasCollection, + viewInsets: widget.viewInsets, + viewPadding: widget.viewPadding, multiPageController: multiPageController, animationController: widget.animationController, ); @@ -89,19 +91,21 @@ class _BottomOverlayContent extends StatefulWidget { final int index; final AvesEntry mainEntry, pageEntry; final bool hasCollection; + final EdgeInsets? viewInsets, viewPadding; final MultiPageController? multiPageController; final AnimationController animationController; const _BottomOverlayContent({ - Key? key, required this.entries, required this.index, required this.mainEntry, required this.pageEntry, required this.hasCollection, + required this.viewInsets, + required this.viewPadding, required this.multiPageController, required this.animationController, - }) : super(key: key); + }); @override State<_BottomOverlayContent> createState() => _BottomOverlayContentState(); @@ -140,11 +144,20 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> { return Selector( selector: (context, mq) => mq.size.width, builder: (context, mqWidth, child) { - final viewerButtonRow = ViewerButtonRow( - mainEntry: mainEntry, - pageEntry: pageEntry, - scale: _buttonScale, - canToggleFavourite: widget.hasCollection, + final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero); + final viewerButtonRow = SafeArea( + top: false, + bottom: false, + minimum: EdgeInsets.only( + left: viewInsetsPadding.left, + right: viewInsetsPadding.right, + ), + child: ViewerButtonRow( + mainEntry: mainEntry, + pageEntry: pageEntry, + scale: _buttonScale, + canToggleFavourite: widget.hasCollection, + ), ); final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null; @@ -211,11 +224,11 @@ class ExtraBottomOverlay extends StatelessWidget { final Widget child; const ExtraBottomOverlay({ - Key? key, + super.key, this.viewInsets, this.viewPadding, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/viewer/overlay/common.dart b/lib/widgets/viewer/overlay/common.dart index 0c94ca5a5..980ef34ca 100644 --- a/lib/widgets/viewer/overlay/common.dart +++ b/lib/widgets/viewer/overlay/common.dart @@ -10,11 +10,11 @@ class OverlayButton extends StatelessWidget { final Widget child; const OverlayButton({ - Key? key, + super.key, this.scale = kAlwaysCompleteAnimation, this.borderRadius, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -66,11 +66,11 @@ class OverlayTextButton extends StatelessWidget { final VoidCallback? onPressed; const OverlayTextButton({ - Key? key, + super.key, required this.scale, required this.buttonLabel, this.onPressed, - }) : super(key: key); + }); static const _borderRadius = 123.0; static final _minSize = MaterialStateProperty.all(const Size(kMinInteractiveDimension, kMinInteractiveDimension)); diff --git a/lib/widgets/viewer/overlay/details.dart b/lib/widgets/viewer/overlay/details.dart index 9ebff5b71..53a8adc74 100644 --- a/lib/widgets/viewer/overlay/details.dart +++ b/lib/widgets/viewer/overlay/details.dart @@ -33,12 +33,12 @@ class ViewerDetailOverlay extends StatefulWidget { final MultiPageController? multiPageController; const ViewerDetailOverlay({ - Key? key, + super.key, required this.entries, required this.index, required this.hasCollection, required this.multiPageController, - }) : super(key: key); + }); @override State createState() => _ViewerDetailOverlayState(); @@ -127,13 +127,13 @@ class ViewerDetailOverlayContent extends StatelessWidget { static const padding = EdgeInsets.symmetric(vertical: 4, horizontal: 8); const ViewerDetailOverlayContent({ - Key? key, + super.key, required this.pageEntry, required this.details, required this.position, required this.availableWidth, required this.multiPageController, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -288,9 +288,8 @@ class _LocationRow extends AnimatedWidget { final AvesEntry entry; _LocationRow({ - Key? key, required this.entry, - }) : super(key: key, listenable: entry.addressChangeNotifier); + }) : super(listenable: entry.addressChangeNotifier); @override Widget build(BuildContext context) { @@ -307,7 +306,7 @@ class _LocationRow extends AnimatedWidget { } return Row( children: [ - DecoratedIcon(AIcons.location, shadows: _shadows(context), size: _iconSize), + DecoratedIcon(AIcons.location, size: _iconSize, shadows: _shadows(context)), const SizedBox(width: _iconPadding), Expanded(child: Text(location, strutStyle: Constants.overflowStrutStyle)), ], @@ -388,7 +387,7 @@ class _DateRow extends StatelessWidget { return Row( children: [ - DecoratedIcon(AIcons.date, shadows: _shadows(context), size: _iconSize), + DecoratedIcon(AIcons.date, size: _iconSize, shadows: _shadows(context)), const SizedBox(width: _iconPadding), Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)), Expanded(flex: 2, child: Text(resolutionText, strutStyle: Constants.overflowStrutStyle)), @@ -417,7 +416,7 @@ class _ShootingRow extends StatelessWidget { return Row( children: [ - DecoratedIcon(AIcons.shooting, shadows: _shadows(context), size: _iconSize), + DecoratedIcon(AIcons.shooting, size: _iconSize, shadows: _shadows(context)), const SizedBox(width: _iconPadding), Expanded(child: Text(apertureText, strutStyle: Constants.overflowStrutStyle)), Expanded(child: Text(details.exposureTime ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)), diff --git a/lib/widgets/viewer/overlay/minimap.dart b/lib/widgets/viewer/overlay/minimap.dart index dad6232ae..b3f5de899 100644 --- a/lib/widgets/viewer/overlay/minimap.dart +++ b/lib/widgets/viewer/overlay/minimap.dart @@ -10,9 +10,9 @@ class Minimap extends StatelessWidget { static const Size minimapSize = Size(96, 96); const Minimap({ - Key? key, + super.key, required this.viewStateNotifier, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/viewer/overlay/multipage.dart b/lib/widgets/viewer/overlay/multipage.dart index 44507cd47..708d542b0 100644 --- a/lib/widgets/viewer/overlay/multipage.dart +++ b/lib/widgets/viewer/overlay/multipage.dart @@ -11,11 +11,11 @@ class MultiPageOverlay extends StatefulWidget { final bool scrollable; const MultiPageOverlay({ - Key? key, + super.key, required this.controller, required this.availableWidth, required this.scrollable, - }) : super(key: key); + }); @override State createState() => _MultiPageOverlayState(); diff --git a/lib/widgets/viewer/overlay/panorama.dart b/lib/widgets/viewer/overlay/panorama.dart index a76b2e9d6..4facceecc 100644 --- a/lib/widgets/viewer/overlay/panorama.dart +++ b/lib/widgets/viewer/overlay/panorama.dart @@ -12,10 +12,10 @@ class PanoramaOverlay extends StatelessWidget { final Animation scale; const PanoramaOverlay({ - Key? key, + super.key, required this.entry, required this.scale, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/viewer/overlay/thumbnail_preview.dart b/lib/widgets/viewer/overlay/thumbnail_preview.dart index 796a1b99b..8ac74c54d 100644 --- a/lib/widgets/viewer/overlay/thumbnail_preview.dart +++ b/lib/widgets/viewer/overlay/thumbnail_preview.dart @@ -11,11 +11,11 @@ class ViewerThumbnailPreview extends StatefulWidget { final double availableWidth; const ViewerThumbnailPreview({ - Key? key, + super.key, required this.entries, required this.displayedIndex, required this.availableWidth, - }) : super(key: key); + }); @override State createState() => _ViewerThumbnailPreviewState(); diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index b7075e7bd..d680f431a 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -3,7 +3,6 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; -import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/overlay/details.dart'; import 'package:aves/widgets/viewer/overlay/minimap.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart'; @@ -16,11 +15,11 @@ class ViewerTopOverlay extends StatelessWidget { final int index; final AvesEntry mainEntry; final Animation scale; - final EdgeInsets? viewInsets, viewPadding; final bool hasCollection; + final EdgeInsets? viewInsets, viewPadding; const ViewerTopOverlay({ - Key? key, + super.key, required this.entries, required this.index, required this.mainEntry, @@ -28,79 +27,68 @@ class ViewerTopOverlay extends StatelessWidget { required this.hasCollection, required this.viewInsets, required this.viewPadding, - }) : super(key: key); + }); @override Widget build(BuildContext context) { - late Widget child; + final multiPageController = mainEntry.isMultiPage ? context.read().getController(mainEntry) : null; + return PageEntryBuilder( + multiPageController: multiPageController, + builder: (pageEntry) { + pageEntry ??= mainEntry; - if (mainEntry.isMultiPage) { - final multiPageController = context.read().getController(mainEntry); - child = PageEntryBuilder( - multiPageController: multiPageController, - builder: (pageEntry) => _buildOverlay( - context, - mainEntry, - pageEntry: pageEntry, - multiPageController: multiPageController, - ), - ); - } else { - child = _buildOverlay(context, mainEntry); - } + final showInfo = settings.showOverlayInfo; - return child; - } + final viewStateConductor = context.read(); + final viewStateNotifier = viewStateConductor.getOrCreateController(pageEntry); - Widget _buildOverlay( - BuildContext context, - AvesEntry mainEntry, { - AvesEntry? pageEntry, - MultiPageController? multiPageController, - }) { - pageEntry ??= mainEntry; - - final showInfo = settings.showOverlayInfo; - - final viewStateConductor = context.read(); - final viewStateNotifier = viewStateConductor.getOrCreateController(pageEntry); - - final blurred = settings.enableOverlayBlurEffect; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (showInfo) - BlurredRect( - enabled: blurred, - child: Container( - color: Themes.overlayBackgroundColor(brightness: Theme.of(context).brightness, blurred: blurred), - child: SafeArea( - minimum: EdgeInsets.only(top: (viewInsets?.top ?? 0) + (viewPadding?.top ?? 0)), - bottom: false, - child: ViewerDetailOverlay( - index: index, - entries: entries, - hasCollection: hasCollection, - multiPageController: multiPageController, + final blurred = settings.enableOverlayBlurEffect; + final viewInsetsPadding = (viewInsets ?? EdgeInsets.zero) + (viewPadding ?? EdgeInsets.zero); + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showInfo) + BlurredRect( + enabled: blurred, + child: Container( + color: Themes.overlayBackgroundColor(brightness: Theme.of(context).brightness, blurred: blurred), + child: SafeArea( + bottom: false, + minimum: EdgeInsets.only( + left: viewInsetsPadding.left, + top: viewInsetsPadding.top, + right: viewInsetsPadding.right, + ), + child: ViewerDetailOverlay( + index: index, + entries: entries, + hasCollection: hasCollection, + multiPageController: multiPageController, + ), + ), ), ), - ), - ), - if (settings.showOverlayMinimap) - SafeArea( - top: !showInfo, - child: Padding( - padding: const EdgeInsets.all(8), - child: FadeTransition( - opacity: scale, - child: Minimap( - viewStateNotifier: viewStateNotifier, + if (settings.showOverlayMinimap) + SafeArea( + top: !showInfo, + minimum: EdgeInsets.only( + left: viewInsetsPadding.left, + right: viewInsetsPadding.right, ), - ), - ), - ) - ], + child: Padding( + padding: const EdgeInsets.all(8), + child: FadeTransition( + opacity: scale, + child: Minimap( + viewStateNotifier: viewStateNotifier, + ), + ), + ), + ) + ], + ); + }, ); } } diff --git a/lib/widgets/viewer/overlay/video/controls.dart b/lib/widgets/viewer/overlay/video/controls.dart index eab6a7be9..90f096566 100644 --- a/lib/widgets/viewer/overlay/video/controls.dart +++ b/lib/widgets/viewer/overlay/video/controls.dart @@ -16,11 +16,11 @@ class VideoControlRow extends StatelessWidget { static const Radius radius = Radius.circular(123); const VideoControlRow({ - Key? key, + super.key, required this.controller, required this.scale, required this.onActionSelected, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/viewer/overlay/video/mute_toggler.dart b/lib/widgets/viewer/overlay/video/mute_toggler.dart index 8ab3089c7..5b7a09350 100644 --- a/lib/widgets/viewer/overlay/video/mute_toggler.dart +++ b/lib/widgets/viewer/overlay/video/mute_toggler.dart @@ -12,11 +12,11 @@ class MuteToggler extends StatelessWidget { final VoidCallback? onPressed; const MuteToggler({ - Key? key, + super.key, required this.controller, this.isMenuItem = false, this.onPressed, - }) : super(key: key); + }); bool get isMuted => controller?.isMuted ?? false; diff --git a/lib/widgets/viewer/overlay/video/play_toggler.dart b/lib/widgets/viewer/overlay/video/play_toggler.dart index b7e781b29..3bce59e4c 100644 --- a/lib/widgets/viewer/overlay/video/play_toggler.dart +++ b/lib/widgets/viewer/overlay/video/play_toggler.dart @@ -14,11 +14,11 @@ class PlayToggler extends StatefulWidget { final VoidCallback? onPressed; const PlayToggler({ - Key? key, + super.key, required this.controller, this.isMenuItem = false, this.onPressed, - }) : super(key: key); + }); @override State createState() => _PlayTogglerState(); diff --git a/lib/widgets/viewer/overlay/video/progress_bar.dart b/lib/widgets/viewer/overlay/video/progress_bar.dart index 185b7e691..ef244e45d 100644 --- a/lib/widgets/viewer/overlay/video/progress_bar.dart +++ b/lib/widgets/viewer/overlay/video/progress_bar.dart @@ -16,10 +16,10 @@ class VideoProgressBar extends StatefulWidget { final Animation scale; const VideoProgressBar({ - Key? key, + super.key, required this.controller, required this.scale, - }) : super(key: key); + }); @override State createState() => _VideoProgressBarState(); diff --git a/lib/widgets/viewer/overlay/video/video.dart b/lib/widgets/viewer/overlay/video/video.dart index 4243cb05c..e0a2d9e4b 100644 --- a/lib/widgets/viewer/overlay/video/video.dart +++ b/lib/widgets/viewer/overlay/video/video.dart @@ -16,13 +16,13 @@ class VideoControlOverlay extends StatefulWidget { final VoidCallback onActionMenuOpened; const VideoControlOverlay({ - Key? key, + super.key, required this.entry, required this.controller, required this.scale, required this.onActionSelected, required this.onActionMenuOpened, - }) : super(key: key); + }); @override State createState() => _VideoControlOverlayState(); diff --git a/lib/widgets/viewer/overlay/viewer_button_row.dart b/lib/widgets/viewer/overlay/viewer_button_row.dart index db1f6a81c..5091cb0e1 100644 --- a/lib/widgets/viewer/overlay/viewer_button_row.dart +++ b/lib/widgets/viewer/overlay/viewer_button_row.dart @@ -36,12 +36,12 @@ class ViewerButtonRow extends StatelessWidget { static double _buttonSize(BuildContext context) => OverlayButton.getSize(context); const ViewerButtonRow({ - Key? key, + super.key, required this.mainEntry, required this.pageEntry, required this.scale, required this.canToggleFavourite, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -147,7 +147,7 @@ class ViewerButtonRowContent extends StatelessWidget { static const double padding = 8; const ViewerButtonRowContent({ - Key? key, + super.key, required this.quickActions, required this.topLevelActions, required this.exportActions, @@ -155,7 +155,7 @@ class ViewerButtonRowContent extends StatelessWidget { required this.scale, required this.mainEntry, required this.pageEntry, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/viewer/page_entry_builder.dart b/lib/widgets/viewer/page_entry_builder.dart index 23050602f..c3b25aca0 100644 --- a/lib/widgets/viewer/page_entry_builder.dart +++ b/lib/widgets/viewer/page_entry_builder.dart @@ -8,28 +8,26 @@ class PageEntryBuilder extends StatelessWidget { final Widget Function(AvesEntry? pageEntry) builder; const PageEntryBuilder({ - Key? key, + super.key, required this.multiPageController, required this.builder, - }) : super(key: key); + }); @override Widget build(BuildContext context) { final controller = multiPageController; - return controller != null - ? StreamBuilder( - stream: controller.infoStream, - builder: (context, snapshot) { - final multiPageInfo = controller.info; - return ValueListenableBuilder( - valueListenable: controller.pageNotifier, - builder: (context, page, child) { - final pageEntry = multiPageInfo?.getPageEntryByIndex(page); - return builder(pageEntry); - }, - ); - }, - ) - : builder(null); + return StreamBuilder( + stream: controller != null ? controller.infoStream : Stream.value(null), + builder: (context, snapshot) { + final multiPageInfo = controller?.info; + return ValueListenableBuilder( + valueListenable: controller?.pageNotifier ?? ValueNotifier(null), + builder: (context, page, child) { + final pageEntry = multiPageInfo?.getPageEntryByIndex(page); + return builder(pageEntry); + }, + ); + }, + ); } } diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index 9aee26ed1..dabe95efb 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -25,10 +25,10 @@ class PanoramaPage extends StatefulWidget { final PanoramaInfo info; const PanoramaPage({ - Key? key, + super.key, required this.entry, required this.info, - }) : super(key: key); + }); @override State createState() => _PanoramaPageState(); @@ -46,7 +46,7 @@ class _PanoramaPageState extends State { void initState() { super.initState(); _overlayVisible.addListener(_onOverlayVisibleChange); - WidgetsBinding.instance!.addPostFrameCallback((_) => _initOverlay()); + WidgetsBinding.instance.addPostFrameCallback((_) => _initOverlay()); } @override diff --git a/lib/widgets/viewer/source_viewer_page.dart b/lib/widgets/viewer/source_viewer_page.dart index f486011ea..18e54ef94 100644 --- a/lib/widgets/viewer/source_viewer_page.dart +++ b/lib/widgets/viewer/source_viewer_page.dart @@ -9,9 +9,9 @@ class SourceViewerPage extends StatefulWidget { final Future Function() loader; const SourceViewerPage({ - Key? key, + super.key, required this.loader, - }) : super(key: key); + }); @override State createState() => _SourceViewerPageState(); diff --git a/lib/widgets/viewer/video/conductor.dart b/lib/widgets/viewer/video/conductor.dart index e0882e562..eac21879b 100644 --- a/lib/widgets/viewer/video/conductor.dart +++ b/lib/widgets/viewer/video/conductor.dart @@ -7,7 +7,7 @@ class VideoConductor { final List _controllers = []; final bool persistPlayback; - static const maxControllerCount = 3; + static const _defaultMaxControllerCount = 3; VideoConductor({required this.persistPlayback}); @@ -16,7 +16,7 @@ class VideoConductor { _controllers.clear(); } - AvesVideoController getOrCreateController(AvesEntry entry) { + AvesVideoController getOrCreateController(AvesEntry entry, {int? maxControllerCount}) { var controller = getController(entry); if (controller != null) { _controllers.remove(controller); @@ -24,7 +24,7 @@ class VideoConductor { controller = IjkPlayerAvesVideoController(entry, persistPlayback: persistPlayback); } _controllers.insert(0, controller); - while (_controllers.length > maxControllerCount) { + while (_controllers.length > (maxControllerCount ?? _defaultMaxControllerCount)) { _controllers.removeLast().dispose(); } return controller; diff --git a/lib/widgets/viewer/video_action_delegate.dart b/lib/widgets/viewer/video_action_delegate.dart index 0fc98c622..d2cfb7e7b 100644 --- a/lib/widgets/viewer/video_action_delegate.dart +++ b/lib/widgets/viewer/video_action_delegate.dart @@ -7,6 +7,7 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/enums.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; @@ -15,7 +16,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/video_speed_dialog.dart'; import 'package:aves/widgets/dialogs/video_stream_selection_dialog.dart'; -import 'package:aves/widgets/settings/video/video.dart'; +import 'package:aves/widgets/settings/video/video_settings_page.dart'; import 'package:aves/widgets/viewer/overlay/notifications.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:collection/collection.dart'; @@ -110,26 +111,28 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix final l10n = context.l10n; if (success) { final _collection = collection; - final navigator = Navigator.of(context); final showAction = _collection != null ? SnackBarAction( label: l10n.showButtonLabel, onPressed: () { - final source = _collection.source; - final newUri = newFields['uri'] as String?; - // `context` may be obsolete if the user navigated away before triggering the action - // so we reused the navigator retrieved before showing the snack bar - navigator.pushAndRemoveUntil( - MaterialPageRoute( - settings: const RouteSettings(name: CollectionPage.routeName), - builder: (context) => CollectionPage( - source: source, - filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))}, - highlightTest: (entry) => entry.uri == newUri, + // local context may be deactivated when action is triggered after navigation + final context = AvesApp.navigatorKey.currentContext; + if (context != null) { + final source = _collection.source; + final newUri = newFields['uri'] as String?; + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + settings: const RouteSettings(name: CollectionPage.routeName), + builder: (context) => CollectionPage( + source: source, + filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))}, + highlightTest: (entry) => entry.uri == newUri, + ), ), - ), - (route) => false, - ); + (route) => false, + ); + } }, ) : null; diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 5d74b5c9a..f642dd8d2 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -39,11 +39,11 @@ class EntryPageView extends StatefulWidget { static const decorationCheckSize = 20.0; const EntryPageView({ - Key? key, + super.key, required this.mainEntry, required this.pageEntry, this.onDisposed, - }) : super(key: key); + }); @override State createState() => _EntryPageViewState(); diff --git a/lib/widgets/viewer/visual/error.dart b/lib/widgets/viewer/visual/error.dart index 0c7a3bd5a..85ce3d3d9 100644 --- a/lib/widgets/viewer/visual/error.dart +++ b/lib/widgets/viewer/visual/error.dart @@ -12,10 +12,10 @@ class ErrorView extends StatefulWidget { final VoidCallback onTap; const ErrorView({ - Key? key, + super.key, required this.entry, required this.onTap, - }) : super(key: key); + }); @override State createState() => _ErrorViewState(); diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index 077df7188..6a8dec32d 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -21,11 +21,11 @@ class RasterImageView extends StatefulWidget { final ImageErrorWidgetBuilder errorBuilder; const RasterImageView({ - Key? key, + super.key, required this.entry, required this.viewStateNotifier, required this.errorBuilder, - }) : super(key: key); + }); @override State createState() => _RasterImageViewState(); @@ -337,12 +337,11 @@ class _RegionTile extends StatefulWidget { final int sampleSize; const _RegionTile({ - Key? key, required this.entry, required this.tileRect, required this.regionRect, required this.sampleSize, - }) : super(key: key); + }); @override State<_RegionTile> createState() => _RegionTileState(); diff --git a/lib/widgets/viewer/visual/subtitle/subtitle.dart b/lib/widgets/viewer/visual/subtitle/subtitle.dart index 6695fc51e..e9a5cac3e 100644 --- a/lib/widgets/viewer/visual/subtitle/subtitle.dart +++ b/lib/widgets/viewer/visual/subtitle/subtitle.dart @@ -19,11 +19,11 @@ class VideoSubtitles extends StatelessWidget { static const baseShadowOffset = Offset(1, 1); const VideoSubtitles({ - Key? key, + super.key, required this.controller, required this.viewStateNotifier, this.debugMode = false, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/viewer/visual/vector.dart b/lib/widgets/viewer/visual/vector.dart index cc0e1277e..64ea730fb 100644 --- a/lib/widgets/viewer/visual/vector.dart +++ b/lib/widgets/viewer/visual/vector.dart @@ -21,11 +21,11 @@ class VectorImageView extends StatefulWidget { final ImageErrorWidgetBuilder errorBuilder; const VectorImageView({ - Key? key, + super.key, required this.entry, required this.viewStateNotifier, required this.errorBuilder, - }) : super(key: key); + }); @override State createState() => _VectorImageViewState(); @@ -290,14 +290,13 @@ class _RegionTile extends StatefulWidget { final _BackgroundFrameBuilder? backgroundFrameBuilder; const _RegionTile({ - Key? key, required this.entry, required this.tileRect, required this.regionRect, required this.scale, required this.backgroundColor, required this.backgroundFrameBuilder, - }) : super(key: key); + }); @override State<_RegionTile> createState() => _RegionTileState(); diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video.dart index ec17a12e4..6fadf9908 100644 --- a/lib/widgets/viewer/visual/video.dart +++ b/lib/widgets/viewer/visual/video.dart @@ -7,10 +7,10 @@ class VideoView extends StatefulWidget { final AvesVideoController controller; const VideoView({ - Key? key, + super.key, required this.entry, required this.controller, - }) : super(key: key); + }); @override State createState() => _VideoViewState(); diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index bd2e635d5..e08c40497 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -15,7 +15,7 @@ import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:provider/provider.dart'; class WelcomePage extends StatefulWidget { - const WelcomePage({Key? key}) : super(key: key); + const WelcomePage({super.key}); @override State createState() => _WelcomePageState(); @@ -33,7 +33,7 @@ class _WelcomePageState extends State { super.initState(); settings.setContextualDefaults(); _termsLoader = rootBundle.loadString(termsPath); - WidgetsBinding.instance!.addPostFrameCallback((_) => _initWelcomeSettings()); + WidgetsBinding.instance.addPostFrameCallback((_) => _initWelcomeSettings()); } // explicitly set consent values to current defaults diff --git a/plugins/aves_map/.gitignore b/plugins/aves_map/.gitignore new file mode 100644 index 000000000..9be145fde --- /dev/null +++ b/plugins/aves_map/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/plugins/aves_map/.metadata b/plugins/aves_map/.metadata new file mode 100644 index 000000000..c24d00d29 --- /dev/null +++ b/plugins/aves_map/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 5464c5bac742001448fe4fc0597be939379f88ea + channel: stable + +project_type: package diff --git a/plugins/aves_map/analysis_options.yaml b/plugins/aves_map/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/plugins/aves_map/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/plugins/aves_map/lib/aves_map.dart b/plugins/aves_map/lib/aves_map.dart new file mode 100644 index 000000000..3de25590d --- /dev/null +++ b/plugins/aves_map/lib/aves_map.dart @@ -0,0 +1,15 @@ +library aves_map; + +export 'src/controller.dart'; +export 'src/geo_entry.dart'; +export 'src/geo_utils.dart'; +export 'src/interface.dart'; +export 'src/marker/dot.dart'; +export 'src/marker/generator.dart'; +export 'src/marker/image.dart'; +export 'src/marker/key.dart'; +export 'src/overlay/overlay.dart'; +export 'src/overlay/tile.dart'; +export 'src/style.dart'; +export 'src/theme.dart'; +export 'src/zoomed_bounds.dart'; diff --git a/lib/widgets/common/map/controller.dart b/plugins/aves_map/lib/src/controller.dart similarity index 95% rename from lib/widgets/common/map/controller.dart rename to plugins/aves_map/lib/src/controller.dart index 4bb280bb2..e196bcf2e 100644 --- a/lib/widgets/common/map/controller.dart +++ b/plugins/aves_map/lib/src/controller.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:aves/widgets/common/map/zoomed_bounds.dart'; +import 'package:aves_map/src/zoomed_bounds.dart'; import 'package:latlong2/latlong.dart'; class AvesMapController { diff --git a/lib/widgets/common/map/geo_entry.dart b/plugins/aves_map/lib/src/geo_entry.dart similarity index 92% rename from lib/widgets/common/map/geo_entry.dart rename to plugins/aves_map/lib/src/geo_entry.dart index 9eab3b881..046ce4c1b 100644 --- a/lib/widgets/common/map/geo_entry.dart +++ b/plugins/aves_map/lib/src/geo_entry.dart @@ -1,9 +1,8 @@ -import 'package:aves/model/entry.dart'; import 'package:fluster/fluster.dart'; import 'package:flutter/foundation.dart'; -class GeoEntry extends Clusterable { - AvesEntry? entry; +class GeoEntry extends Clusterable { + T? entry; GeoEntry({ this.entry, diff --git a/lib/utils/geo_utils.dart b/plugins/aves_map/lib/src/geo_utils.dart similarity index 89% rename from lib/utils/geo_utils.dart rename to plugins/aves_map/lib/src/geo_utils.dart index a82a1ff2e..5272482f5 100644 --- a/lib/utils/geo_utils.dart +++ b/plugins/aves_map/lib/src/geo_utils.dart @@ -1,27 +1,8 @@ import 'dart:math'; -import 'package:intl/intl.dart'; import 'package:latlong2/latlong.dart'; class GeoUtils { - static String decimal2sexagesimal( - double degDecimal, - bool minuteSecondPadding, - int secondDecimals, - String locale, - ) { - final degAbs = degDecimal.abs(); - final deg = degAbs.toInt(); - final minDecimal = (degAbs - deg) * 60; - final min = minDecimal.toInt(); - final sec = (minDecimal - min) * 60; - - var minText = NumberFormat('0' * (minuteSecondPadding ? 2 : 1), locale).format(min); - var secText = NumberFormat('${'0' * (minuteSecondPadding ? 2 : 1)}${secondDecimals > 0 ? '.${'0' * secondDecimals}' : ''}', locale).format(sec); - - return '$deg° $minText′ $secText″'; - } - static LatLng getLatLngCenter(List points) { double x = 0; double y = 0; diff --git a/plugins/aves_map/lib/src/interface.dart b/plugins/aves_map/lib/src/interface.dart new file mode 100644 index 000000000..f2c31eb02 --- /dev/null +++ b/plugins/aves_map/lib/src/interface.dart @@ -0,0 +1,12 @@ +import 'package:aves_map/src/geo_entry.dart'; +import 'package:aves_map/src/marker/key.dart'; +import 'package:flutter/material.dart'; +import 'package:latlong2/latlong.dart'; + +typedef ButtonPanelBuilder = Widget Function(Future Function(double amount) zoomBy, VoidCallback resetRotation); +typedef MarkerClusterBuilder = Map, GeoEntry> Function(); +typedef MarkerWidgetBuilder = Widget Function(MarkerKey key); +typedef MarkerImageReadyChecker = bool Function(MarkerKey key); +typedef UserZoomChangeCallback = void Function(double zoom); +typedef MapTapCallback = void Function(LatLng location); +typedef MarkerTapCallback = void Function(GeoEntry geoEntry); diff --git a/plugins/aves_map/lib/src/marker/dot.dart b/plugins/aves_map/lib/src/marker/dot.dart new file mode 100644 index 000000000..7a3477e22 --- /dev/null +++ b/plugins/aves_map/lib/src/marker/dot.dart @@ -0,0 +1,54 @@ +import 'package:aves_map/src/theme.dart'; +import 'package:flutter/material.dart'; + +class DotMarker extends StatelessWidget { + const DotMarker({super.key}); + + static const double diameter = 16; + static const double outerBorderRadiusDim = diameter; + static const double outerBorderWidth = MapThemeData.markerOuterBorderWidth; + static const double innerBorderWidth = MapThemeData.markerInnerBorderWidth; + static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim)); + static const innerRadius = Radius.circular(outerBorderRadiusDim - outerBorderWidth); + static const innerBorderRadius = BorderRadius.all(innerRadius); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final outerBorderColor = MapThemeData.markerThemedOuterBorderColor(isDark); + final innerBorderColor = MapThemeData.markerThemedInnerBorderColor(isDark); + + final outerDecoration = BoxDecoration( + border: Border.fromBorderSide(BorderSide( + color: outerBorderColor, + width: outerBorderWidth, + )), + borderRadius: outerBorderRadius, + ); + + final innerDecoration = BoxDecoration( + border: Border.fromBorderSide(BorderSide( + color: innerBorderColor, + width: innerBorderWidth, + )), + borderRadius: innerBorderRadius, + ); + + return Container( + decoration: outerDecoration, + child: DecoratedBox( + decoration: innerDecoration, + position: DecorationPosition.foreground, + child: ClipRRect( + borderRadius: innerBorderRadius, + child: Container( + width: diameter, + height: diameter, + color: theme.colorScheme.secondary, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/common/map/google/marker_generator.dart b/plugins/aves_map/lib/src/marker/generator.dart similarity index 97% rename from lib/widgets/common/map/google/marker_generator.dart rename to plugins/aves_map/lib/src/marker/generator.dart index 55f093b2f..e8a8f77c0 100644 --- a/lib/widgets/common/map/google/marker_generator.dart +++ b/plugins/aves_map/lib/src/marker/generator.dart @@ -14,11 +14,11 @@ class MarkerGeneratorWidget extends StatefulWidget { final void Function(T markerKey, Uint8List bitmap) onRendered; const MarkerGeneratorWidget({ - Key? key, + super.key, required this.markers, required this.isReadyToRender, required this.onRendered, - }) : super(key: key); + }); @override State> createState() => _MarkerGeneratorWidgetState(); @@ -44,7 +44,7 @@ class _MarkerGeneratorWidgetState extends State v.isWaiting).toSet(); final readyItems = waitingItems.where((v) => widget.isReadyToRender(v.markerKey)).toSet(); diff --git a/lib/widgets/common/map/marker.dart b/plugins/aves_map/lib/src/marker/image.dart similarity index 64% rename from lib/widgets/common/map/marker.dart rename to plugins/aves_map/lib/src/marker/image.dart index ddbfb9cad..08ccc4b5a 100644 --- a/lib/widgets/common/map/marker.dart +++ b/plugins/aves_map/lib/src/marker/image.dart @@ -1,45 +1,29 @@ -import 'package:aves/model/entry.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/thumbnail/image.dart'; +import 'package:aves_map/src/theme.dart'; import 'package:custom_rounded_rectangle_border/custom_rounded_rectangle_border.dart'; import 'package:flutter/material.dart'; class ImageMarker extends StatelessWidget { - final AvesEntry? entry; final int? count; - final double extent; - final Size arrowSize; - final bool progressive; + final Widget Function(double extent) buildThumbnailImage; static const double outerBorderRadiusDim = 8; - static const double outerBorderWidth = 1.5; - static const double innerBorderWidth = 2; + static const outerBorderWidth = MapThemeData.markerOuterBorderWidth; + static const innerBorderWidth = MapThemeData.markerInnerBorderWidth; + static const extent = MapThemeData.markerImageExtent; + static const arrowSize = MapThemeData.markerArrowSize; static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim)); static const innerRadius = Radius.circular(outerBorderRadiusDim - outerBorderWidth); static const innerBorderRadius = BorderRadius.all(innerRadius); - static Color themedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26; - - static Color themedInnerBorderColor(bool isDark) => isDark ? const Color(0xFF212121) : Colors.white; - const ImageMarker({ - Key? key, - required this.entry, + super.key, required this.count, - required this.extent, - required this.arrowSize, - required this.progressive, - }) : super(key: key); + required this.buildThumbnailImage, + }); @override Widget build(BuildContext context) { - Widget child = entry != null - ? ThumbnailImage( - entry: entry!, - extent: extent, - progressive: progressive, - ) - : const SizedBox(); + Widget child = buildThumbnailImage(extent); // need to be sized for the Google map marker generator child = SizedBox( @@ -50,8 +34,8 @@ class ImageMarker extends StatelessWidget { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; - final outerBorderColor = themedOuterBorderColor(isDark); - final innerBorderColor = themedInnerBorderColor(isDark); + final outerBorderColor = MapThemeData.markerThemedOuterBorderColor(isDark); + final innerBorderColor = MapThemeData.markerThemedInnerBorderColor(isDark); final outerDecoration = BoxDecoration( border: Border.fromBorderSide(BorderSide( @@ -90,7 +74,7 @@ class ImageMarker extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 2), decoration: ShapeDecoration( color: theme.colorScheme.secondary, - shape: context.isRtl + shape: Directionality.of(context) == TextDirection.rtl ? CustomRoundedRectangleBorder( leftSide: borderSide, rightSide: borderSide, @@ -188,53 +172,3 @@ class _MarkerArrowPainter extends CustomPainter { @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } - -class DotMarker extends StatelessWidget { - const DotMarker({Key? key}) : super(key: key); - - static const double diameter = 16; - static const double outerBorderRadiusDim = diameter; - static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim)); - static const innerRadius = Radius.circular(outerBorderRadiusDim - ImageMarker.outerBorderWidth); - static const innerBorderRadius = BorderRadius.all(innerRadius); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - final outerBorderColor = ImageMarker.themedOuterBorderColor(isDark); - final innerBorderColor = ImageMarker.themedInnerBorderColor(isDark); - - final outerDecoration = BoxDecoration( - border: Border.fromBorderSide(BorderSide( - color: outerBorderColor, - width: ImageMarker.outerBorderWidth, - )), - borderRadius: outerBorderRadius, - ); - - final innerDecoration = BoxDecoration( - border: Border.fromBorderSide(BorderSide( - color: innerBorderColor, - width: ImageMarker.innerBorderWidth, - )), - borderRadius: innerBorderRadius, - ); - - return Container( - decoration: outerDecoration, - child: DecoratedBox( - decoration: innerDecoration, - position: DecorationPosition.foreground, - child: ClipRRect( - borderRadius: innerBorderRadius, - child: Container( - width: diameter, - height: diameter, - color: theme.colorScheme.secondary, - ), - ), - ), - ); - } -} diff --git a/plugins/aves_map/lib/src/marker/key.dart b/plugins/aves_map/lib/src/marker/key.dart new file mode 100644 index 000000000..188ec010f --- /dev/null +++ b/plugins/aves_map/lib/src/marker/key.dart @@ -0,0 +1,13 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +@immutable +class MarkerKey extends LocalKey with EquatableMixin { + final T entry; + final int? count; + + @override + List get props => [entry, count]; + + const MarkerKey(this.entry, this.count); +} diff --git a/plugins/aves_map/lib/src/overlay/overlay.dart b/plugins/aves_map/lib/src/overlay/overlay.dart new file mode 100644 index 000000000..6fafa66f5 --- /dev/null +++ b/plugins/aves_map/lib/src/overlay/overlay.dart @@ -0,0 +1,17 @@ +import 'package:aves_map/src/overlay/tile.dart'; +import 'package:flutter/painting.dart'; +import 'package:latlong2/latlong.dart'; + +mixin MapOverlay { + String get id; + + bool get canOverlay; + + LatLng? get topLeft; + + LatLng? get bottomRight; + + ImageProvider get imageProvider; + + Future getTile(int tx, int ty, int? zoomLevel); +} diff --git a/lib/widgets/common/map/tile.dart b/plugins/aves_map/lib/src/overlay/tile.dart similarity index 100% rename from lib/widgets/common/map/tile.dart rename to plugins/aves_map/lib/src/overlay/tile.dart diff --git a/plugins/aves_map/lib/src/style.dart b/plugins/aves_map/lib/src/style.dart new file mode 100644 index 000000000..60cf96799 --- /dev/null +++ b/plugins/aves_map/lib/src/style.dart @@ -0,0 +1,14 @@ +enum EntryMapStyle { + // Google + googleNormal, + googleHybrid, + googleTerrain, + // Huawei + hmsNormal, + hmsTerrain, + // Leaflet + // browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ + osmHot, + stamenToner, + stamenWatercolor, +} diff --git a/plugins/aves_map/lib/src/theme.dart b/plugins/aves_map/lib/src/theme.dart new file mode 100644 index 000000000..c0f75e67b --- /dev/null +++ b/plugins/aves_map/lib/src/theme.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +enum MapNavigationButton { back, map } + +class MapThemeData { + final bool interactive, showCoordinateFilter; + final MapNavigationButton navigationButton; + final Animation scale; + final VisualDensity? visualDensity; + final double? mapHeight; + + const MapThemeData({ + required this.interactive, + required this.showCoordinateFilter, + required this.navigationButton, + required this.scale, + required this.visualDensity, + required this.mapHeight, + }); + + static const double markerOuterBorderWidth = 1.5; + static const double markerInnerBorderWidth = 2; + static const double markerImageExtent = 48.0; + static const Size markerArrowSize = Size(8, 6); + + static Color markerThemedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26; + + static Color markerThemedInnerBorderColor(bool isDark) => isDark ? const Color(0xFF212121) : Colors.white; +} diff --git a/lib/widgets/common/map/zoomed_bounds.dart b/plugins/aves_map/lib/src/zoomed_bounds.dart similarity index 98% rename from lib/widgets/common/map/zoomed_bounds.dart rename to plugins/aves_map/lib/src/zoomed_bounds.dart index b0d375145..0f835e3f4 100644 --- a/lib/widgets/common/map/zoomed_bounds.dart +++ b/plugins/aves_map/lib/src/zoomed_bounds.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:aves/utils/geo_utils.dart'; +import 'package:aves_map/src/geo_utils.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_map/flutter_map.dart'; diff --git a/plugins/aves_map/pubspec.yaml b/plugins/aves_map/pubspec.yaml new file mode 100644 index 000000000..68b1ea5e4 --- /dev/null +++ b/plugins/aves_map/pubspec.yaml @@ -0,0 +1,23 @@ +name: aves_map +version: 0.0.1 +publish_to: none + +environment: + sdk: ">=2.17.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + collection: + # TODO TLAD as of 2022/02/22, null safe version is pre-release + custom_rounded_rectangle_border: '>=0.2.0-nullsafety.0' + equatable: + fluster: + flutter_map: + latlong2: + provider: + +dev_dependencies: + flutter_lints: + +flutter: diff --git a/plugins/aves_report/.gitignore b/plugins/aves_report/.gitignore index e9dc58d3d..9be145fde 100644 --- a/plugins/aves_report/.gitignore +++ b/plugins/aves_report/.gitignore @@ -1,7 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp .DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ .dart_tool/ - .packages -.pub/ - build/ diff --git a/plugins/aves_report/pubspec.lock b/plugins/aves_report/pubspec.lock deleted file mode 100644 index ae1eee690..000000000 --- a/plugins/aves_report/pubspec.lock +++ /dev/null @@ -1,65 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - lints: - dependency: transitive - description: - name: lints - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" -sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=1.20.0" diff --git a/plugins/aves_report/pubspec.yaml b/plugins/aves_report/pubspec.yaml index 5ed692860..ab1c3d7dc 100644 --- a/plugins/aves_report/pubspec.yaml +++ b/plugins/aves_report/pubspec.yaml @@ -3,8 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" + sdk: ">=2.17.0 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_report_console/.gitignore b/plugins/aves_report_console/.gitignore index a247422ef..9be145fde 100644 --- a/plugins/aves_report_console/.gitignore +++ b/plugins/aves_report_console/.gitignore @@ -21,55 +21,9 @@ #.vscode/ # Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock **/doc/api/ .dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies .packages -.pub-cache/ -.pub/ build/ - -# Android related -**/android/**/gradle-wrapper.jar -**/android/.gradle -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java - -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Flutter.podspec -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/ephemeral -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 diff --git a/plugins/aves_report_console/pubspec.lock b/plugins/aves_report_console/pubspec.lock deleted file mode 100644 index 9da951800..000000000 --- a/plugins/aves_report_console/pubspec.lock +++ /dev/null @@ -1,72 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - aves_report: - dependency: "direct main" - description: - path: "../aves_report" - relative: true - source: path - version: "0.0.1" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - lints: - dependency: transitive - description: - name: lints - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" -sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=1.20.0" diff --git a/plugins/aves_report_console/pubspec.yaml b/plugins/aves_report_console/pubspec.yaml index 00e9bccb9..93bcab68f 100644 --- a/plugins/aves_report_console/pubspec.yaml +++ b/plugins/aves_report_console/pubspec.yaml @@ -3,8 +3,7 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.17.0" + sdk: ">=2.17.0 <3.0.0" dependencies: flutter: diff --git a/plugins/aves_report_crashlytics/.gitignore b/plugins/aves_report_crashlytics/.gitignore index a247422ef..9be145fde 100644 --- a/plugins/aves_report_crashlytics/.gitignore +++ b/plugins/aves_report_crashlytics/.gitignore @@ -21,55 +21,9 @@ #.vscode/ # Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock **/doc/api/ .dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies .packages -.pub-cache/ -.pub/ build/ - -# Android related -**/android/**/gradle-wrapper.jar -**/android/.gradle -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java - -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Flutter.podspec -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/ephemeral -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 diff --git a/plugins/aves_report_crashlytics/pubspec.lock b/plugins/aves_report_crashlytics/pubspec.lock deleted file mode 100644 index 0fc3afe8b..000000000 --- a/plugins/aves_report_crashlytics/pubspec.lock +++ /dev/null @@ -1,140 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - aves_report: - dependency: "direct main" - description: - path: "../aves_report" - relative: true - source: path - version: "0.0.1" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - firebase_core: - dependency: "direct main" - description: - name: firebase_core - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.5" - firebase_core_platform_interface: - dependency: transitive - description: - name: firebase_core_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "4.2.2" - firebase_core_web: - dependency: transitive - description: - name: firebase_core_web - url: "https://pub.dartlang.org" - source: hosted - version: "1.5.2" - firebase_crashlytics: - dependency: "direct main" - description: - name: firebase_crashlytics - url: "https://pub.dartlang.org" - source: hosted - version: "2.4.3" - firebase_crashlytics_platform_interface: - dependency: transitive - description: - name: firebase_crashlytics_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.10" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - js: - dependency: transitive - description: - name: js - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.3" - lints: - dependency: transitive - description: - name: lints - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - stack_trace: - dependency: "direct main" - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" -sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=1.20.0" diff --git a/plugins/aves_report_crashlytics/pubspec.yaml b/plugins/aves_report_crashlytics/pubspec.yaml index 6e756d446..981e7f044 100644 --- a/plugins/aves_report_crashlytics/pubspec.yaml +++ b/plugins/aves_report_crashlytics/pubspec.yaml @@ -3,14 +3,14 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.17.0" + sdk: ">=2.17.0 <3.0.0" dependencies: flutter: sdk: flutter aves_report: path: ../aves_report + collection: firebase_core: firebase_crashlytics: stack_trace: diff --git a/plugins/aves_services/.gitignore b/plugins/aves_services/.gitignore new file mode 100644 index 000000000..9be145fde --- /dev/null +++ b/plugins/aves_services/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/plugins/aves_services/.metadata b/plugins/aves_services/.metadata new file mode 100644 index 000000000..c24d00d29 --- /dev/null +++ b/plugins/aves_services/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 5464c5bac742001448fe4fc0597be939379f88ea + channel: stable + +project_type: package diff --git a/plugins/aves_services/analysis_options.yaml b/plugins/aves_services/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/plugins/aves_services/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/plugins/aves_services/lib/aves_services.dart b/plugins/aves_services/lib/aves_services.dart new file mode 100644 index 000000000..200c6ead2 --- /dev/null +++ b/plugins/aves_services/lib/aves_services.dart @@ -0,0 +1,33 @@ +library aves_services; + +import 'package:aves_map/aves_map.dart'; +import 'package:flutter/widgets.dart'; +import 'package:latlong2/latlong.dart'; + +abstract class MobileServices { + Future init(); + + bool get isServiceAvailable; + + EntryMapStyle get defaultMapStyle; + + List get mapStyles; + + Widget buildMap({ + required AvesMapController? controller, + required Listenable clusterListenable, + required ValueNotifier boundsNotifier, + required EntryMapStyle style, + required TransitionBuilder decoratorBuilder, + required ButtonPanelBuilder buttonPanelBuilder, + required MarkerClusterBuilder markerClusterBuilder, + required MarkerWidgetBuilder markerWidgetBuilder, + required MarkerImageReadyChecker markerImageReadyChecker, + required ValueNotifier? dotLocationNotifier, + required ValueNotifier? overlayOpacityNotifier, + required MapOverlay? overlayEntry, + required UserZoomChangeCallback? onUserZoomChange, + required MapTapCallback? onMapTap, + required MarkerTapCallback? onMarkerTap, + }); +} diff --git a/plugins/aves_services/pubspec.yaml b/plugins/aves_services/pubspec.yaml new file mode 100644 index 000000000..6fe9fcf64 --- /dev/null +++ b/plugins/aves_services/pubspec.yaml @@ -0,0 +1,18 @@ +name: aves_services +version: 0.0.1 +publish_to: none + +environment: + sdk: ">=2.17.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + aves_map: + path: ../aves_map + latlong2: + +dev_dependencies: + flutter_lints: + +flutter: diff --git a/plugins/aves_services_google/.gitignore b/plugins/aves_services_google/.gitignore new file mode 100644 index 000000000..9be145fde --- /dev/null +++ b/plugins/aves_services_google/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/plugins/aves_services_google/.metadata b/plugins/aves_services_google/.metadata new file mode 100644 index 000000000..c24d00d29 --- /dev/null +++ b/plugins/aves_services_google/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 5464c5bac742001448fe4fc0597be939379f88ea + channel: stable + +project_type: package diff --git a/plugins/aves_services_google/analysis_options.yaml b/plugins/aves_services_google/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/plugins/aves_services_google/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/plugins/aves_services_google/lib/aves_services_platform.dart b/plugins/aves_services_google/lib/aves_services_platform.dart new file mode 100644 index 000000000..4bc46e554 --- /dev/null +++ b/plugins/aves_services_google/lib/aves_services_platform.dart @@ -0,0 +1,82 @@ +library aves_services_platform; + +import 'package:aves_map/aves_map.dart'; +import 'package:aves_services/aves_services.dart'; +import 'package:aves_services_platform/src/map.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/widgets.dart'; +import 'package:google_api_availability/google_api_availability.dart'; +import 'package:latlong2/latlong.dart'; + +class PlatformMobileServices extends MobileServices { + bool _isAvailable = false; + bool _canRenderMaps = false; + + @override + Future init() async { + final result = await GoogleApiAvailability.instance.checkGooglePlayServicesAvailability(); + _isAvailable = result == GooglePlayServicesAvailability.success; + debugPrint('Device has Google Play Services=$_isAvailable'); + + // as of google_maps_flutter v2.1.1, minSDK is 20 because of default PlatformView usage, + // but using hybrid composition would make it usable on API 19 too, + // cf https://github.com/flutter/flutter/issues/23728 + // as of google_maps_flutter v2.1.5, Flutter v3.0.1 makes the map hide overlay widgets on API <=22 + final androidInfo = await DeviceInfoPlugin().androidInfo; + _canRenderMaps = (androidInfo.version.sdkInt ?? 0) >= 23; + } + + @override + bool get isServiceAvailable => _isAvailable; + + @override + EntryMapStyle get defaultMapStyle => EntryMapStyle.googleNormal; + + @override + List get mapStyles => (isServiceAvailable && _canRenderMaps) + ? [ + EntryMapStyle.googleNormal, + EntryMapStyle.googleHybrid, + EntryMapStyle.googleTerrain, + ] + : []; + + @override + Widget buildMap({ + required AvesMapController? controller, + required Listenable clusterListenable, + required ValueNotifier boundsNotifier, + required EntryMapStyle style, + required TransitionBuilder decoratorBuilder, + required ButtonPanelBuilder buttonPanelBuilder, + required MarkerClusterBuilder markerClusterBuilder, + required MarkerWidgetBuilder markerWidgetBuilder, + required MarkerImageReadyChecker markerImageReadyChecker, + required ValueNotifier? dotLocationNotifier, + required ValueNotifier? overlayOpacityNotifier, + required MapOverlay? overlayEntry, + required UserZoomChangeCallback? onUserZoomChange, + required MapTapCallback? onMapTap, + required MarkerTapCallback? onMarkerTap, + }) { + return EntryGoogleMap( + controller: controller, + clusterListenable: clusterListenable, + boundsNotifier: boundsNotifier, + minZoom: 0, + maxZoom: 20, + style: style, + decoratorBuilder: decoratorBuilder, + buttonPanelBuilder: buttonPanelBuilder, + markerClusterBuilder: markerClusterBuilder, + markerWidgetBuilder: markerWidgetBuilder, + markerImageReadyChecker: markerImageReadyChecker, + dotLocationNotifier: dotLocationNotifier, + overlayOpacityNotifier: overlayOpacityNotifier, + overlayEntry: overlayEntry, + onUserZoomChange: onUserZoomChange, + onMapTap: onMapTap, + onMarkerTap: onMarkerTap, + ); + } +} diff --git a/plugins/aves_services_google/lib/src/map.dart b/plugins/aves_services_google/lib/src/map.dart new file mode 100644 index 000000000..173ee16d3 --- /dev/null +++ b/plugins/aves_services_google/lib/src/map.dart @@ -0,0 +1,338 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:aves_map/aves_map.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:latlong2/latlong.dart' as ll; +import 'package:provider/provider.dart'; + +class EntryGoogleMap extends StatefulWidget { + final AvesMapController? controller; + final Listenable clusterListenable; + final ValueNotifier boundsNotifier; + final double? minZoom, maxZoom; + final EntryMapStyle style; + final TransitionBuilder decoratorBuilder; + final ButtonPanelBuilder buttonPanelBuilder; + final MarkerClusterBuilder markerClusterBuilder; + final MarkerWidgetBuilder markerWidgetBuilder; + final MarkerImageReadyChecker markerImageReadyChecker; + final ValueNotifier? dotLocationNotifier; + final ValueNotifier? overlayOpacityNotifier; + final MapOverlay? overlayEntry; + final UserZoomChangeCallback? onUserZoomChange; + final MapTapCallback? onMapTap; + final MarkerTapCallback? onMarkerTap; + + const EntryGoogleMap({ + super.key, + this.controller, + required this.clusterListenable, + required this.boundsNotifier, + this.minZoom, + this.maxZoom, + required this.style, + required this.decoratorBuilder, + required this.buttonPanelBuilder, + required this.markerClusterBuilder, + required this.markerWidgetBuilder, + required this.markerImageReadyChecker, + required this.dotLocationNotifier, + this.overlayOpacityNotifier, + this.overlayEntry, + this.onUserZoomChange, + this.onMapTap, + this.onMarkerTap, + }); + + @override + State createState() => _EntryGoogleMapState(); +} + +class _EntryGoogleMapState extends State> with WidgetsBindingObserver { + GoogleMapController? _serviceMapController; + final List _subscriptions = []; + Map, GeoEntry> _geoEntryByMarkerKey = {}; + final Map, Uint8List> _markerBitmaps = {}; + final StreamController> _markerBitmapReadyStreamController = StreamController.broadcast(); + Uint8List? _dotMarkerBitmap; + final ValueNotifier _sizeNotifier = ValueNotifier(Size.zero); + + ValueNotifier get boundsNotifier => widget.boundsNotifier; + + ZoomedBounds get bounds => boundsNotifier.value; + + static const uninitializedLatLng = LatLng(0, 0); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _sizeNotifier.addListener(_onSizeChange); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant EntryGoogleMap oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + _serviceMapController?.dispose(); + WidgetsBinding.instance.removeObserver(this); + _sizeNotifier.removeListener(_onSizeChange); + super.dispose(); + } + + void _registerWidget(EntryGoogleMap widget) { + final avesMapController = widget.controller; + if (avesMapController != null) { + _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng)))); + } + widget.clusterListenable.addListener(_updateMarkers); + } + + void _unregisterWidget(EntryGoogleMap widget) { + widget.clusterListenable.removeListener(_updateMarkers); + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.inactive: + case AppLifecycleState.paused: + case AppLifecycleState.detached: + break; + case AppLifecycleState.resumed: + // workaround for blank map when resuming app + // cf https://github.com/flutter/flutter/issues/40284 + _serviceMapController?.setMapStyle(null); + break; + } + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + MarkerGeneratorWidget( + markers: const [DotMarker(key: Key('dot'))], + isReadyToRender: (key) => true, + onRendered: (key, bitmap) => _dotMarkerBitmap = bitmap, + ), + MarkerGeneratorWidget>( + markers: _geoEntryByMarkerKey.keys.map(widget.markerWidgetBuilder).toList(), + isReadyToRender: widget.markerImageReadyChecker, + onRendered: (key, bitmap) { + _markerBitmaps[key] = bitmap; + _markerBitmapReadyStreamController.add(key); + }, + ), + widget.decoratorBuilder(context, _buildMap()), + widget.buttonPanelBuilder(_zoomBy, _resetRotation), + ], + ); + } + + Widget _buildMap() { + return StreamBuilder( + stream: _markerBitmapReadyStreamController.stream, + builder: (context, _) { + final markers = {}; + _geoEntryByMarkerKey.forEach((markerKey, geoEntry) { + final bytes = _markerBitmaps[markerKey]; + if (bytes != null) { + final point = LatLng(geoEntry.latitude!, geoEntry.longitude!); + markers.add(Marker( + markerId: MarkerId(geoEntry.markerId!), + consumeTapEvents: true, + icon: BitmapDescriptor.fromBytes(bytes), + position: point, + onTap: () => widget.onMarkerTap?.call(geoEntry), + )); + } + }); + + final interactive = context.select((v) => v.interactive); + final overlayEntry = widget.overlayEntry; + return ValueListenableBuilder( + valueListenable: widget.dotLocationNotifier ?? ValueNotifier(null), + builder: (context, dotLocation, child) { + return ValueListenableBuilder( + valueListenable: widget.overlayOpacityNotifier ?? ValueNotifier(1), + builder: (context, overlayOpacity, child) { + return LayoutBuilder( + builder: (context, constraints) { + _sizeNotifier.value = constraints.biggest; + return GoogleMap( + initialCameraPosition: CameraPosition( + bearing: -bounds.rotation, + target: _toServiceLatLng(bounds.projectedCenter), + zoom: bounds.zoom, + ), + onMapCreated: (controller) async { + _serviceMapController = controller; + final zoom = await controller.getZoomLevel(); + await _updateVisibleRegion(zoom: zoom, rotation: bounds.rotation); + if (mounted) { + setState(() {}); + } + }, + // compass disabled to use provider agnostic controls + compassEnabled: false, + mapToolbarEnabled: false, + mapType: _toMapType(widget.style), + minMaxZoomPreference: MinMaxZoomPreference(widget.minZoom, widget.maxZoom), + rotateGesturesEnabled: true, + scrollGesturesEnabled: interactive, + // zoom controls disabled to use provider agnostic controls + zoomControlsEnabled: false, + zoomGesturesEnabled: interactive, + // lite mode disabled because it lacks camera animation + liteModeEnabled: false, + // tilt disabled to match leaflet + tiltGesturesEnabled: false, + myLocationEnabled: false, + myLocationButtonEnabled: false, + markers: { + // TODO TLAD workaround for dot location marker not showing the last value until this is fixed: https://github.com/flutter/flutter/issues/103686 + ...markers, + if (dotLocation != null && _dotMarkerBitmap != null) + Marker( + markerId: const MarkerId('dot'), + anchor: const Offset(.5, .5), + consumeTapEvents: true, + icon: BitmapDescriptor.fromBytes(_dotMarkerBitmap!), + position: _toServiceLatLng(dotLocation), + zIndex: 1, + ) + }, + // TODO TLAD [geotiff] may use ground overlay instead when this is fixed: https://github.com/flutter/flutter/issues/26479 + tileOverlays: { + if (overlayEntry != null && overlayEntry.canOverlay) + TileOverlay( + tileOverlayId: TileOverlayId(overlayEntry.id), + tileProvider: GmsGeoTiffTileProvider(overlayEntry), + transparency: 1 - overlayOpacity, + ), + }, + onCameraMove: (position) => _updateVisibleRegion(zoom: position.zoom, rotation: -position.bearing), + onCameraIdle: _onIdle, + onTap: (position) => widget.onMapTap?.call(_fromServiceLatLng(position)), + ); + }, + ); + }, + ); + }, + ); + }, + ); + } + + // sometimes the map does not properly update after changing the widget size, + // so we monitor the size and force refreshing after an arbitrary small delay + Future _onSizeChange() async { + await Future.delayed(const Duration(milliseconds: 100)); + debugPrint('refresh map for size=${_sizeNotifier.value}'); + await _serviceMapController?.setMapStyle(null); + } + + void _onIdle() { + if (!mounted) return; + widget.controller?.notifyIdle(bounds); + _updateMarkers(); + } + + void _updateMarkers() { + setState(() => _geoEntryByMarkerKey = widget.markerClusterBuilder()); + } + + Future _updateVisibleRegion({required double zoom, required double rotation}) async { + if (!mounted) return; + + final bounds = await _serviceMapController?.getVisibleRegion(); + if (bounds != null && (bounds.northeast != uninitializedLatLng || bounds.southwest != uninitializedLatLng)) { + final sw = bounds.southwest; + final ne = bounds.northeast; + boundsNotifier.value = ZoomedBounds( + sw: _fromServiceLatLng(sw), + ne: _fromServiceLatLng(ne), + zoom: zoom, + rotation: rotation, + ); + } else { + // the visible region is sometimes uninitialized when queried right after creation, + // so we query it again next frame + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateVisibleRegion(zoom: zoom, rotation: rotation); + }); + } + } + + Future _resetRotation() async { + final controller = _serviceMapController; + if (controller == null) return; + + await controller.animateCamera(CameraUpdate.newCameraPosition(CameraPosition( + target: _toServiceLatLng(bounds.projectedCenter), + zoom: bounds.zoom, + ))); + } + + Future _zoomBy(double amount) async { + final controller = _serviceMapController; + if (controller == null) return; + + widget.onUserZoomChange?.call(await controller.getZoomLevel() + amount); + await controller.animateCamera(CameraUpdate.zoomBy(amount)); + } + + Future _moveTo(LatLng point) async { + final controller = _serviceMapController; + if (controller == null) return; + + await controller.animateCamera(CameraUpdate.newLatLng(point)); + } + + // `LatLng` used by `google_maps_flutter` is not the one from `latlong2` package + LatLng _toServiceLatLng(ll.LatLng location) => LatLng(location.latitude, location.longitude); + + ll.LatLng _fromServiceLatLng(LatLng location) => ll.LatLng(location.latitude, location.longitude); + + MapType _toMapType(EntryMapStyle style) { + switch (style) { + case EntryMapStyle.googleNormal: + return MapType.normal; + case EntryMapStyle.googleHybrid: + return MapType.hybrid; + case EntryMapStyle.googleTerrain: + return MapType.terrain; + default: + return MapType.none; + } + } +} + +class GmsGeoTiffTileProvider extends TileProvider { + MapOverlay overlayEntry; + + GmsGeoTiffTileProvider(this.overlayEntry); + + @override + Future getTile(int x, int y, int? zoom) async { + final tile = await overlayEntry.getTile(x, y, zoom); + if (tile != null) { + return Tile(tile.width, tile.height, tile.data); + } + return TileProvider.noTile; + } +} diff --git a/plugins/aves_services_google/pubspec.yaml b/plugins/aves_services_google/pubspec.yaml new file mode 100644 index 000000000..b2b3f16f1 --- /dev/null +++ b/plugins/aves_services_google/pubspec.yaml @@ -0,0 +1,24 @@ +name: aves_services_platform +version: 0.0.1 +publish_to: none + +environment: + sdk: ">=2.17.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + aves_map: + path: ../aves_map + aves_services: + path: ../aves_services + device_info_plus: + google_api_availability: + google_maps_flutter: + latlong2: + provider: + +dev_dependencies: + flutter_lints: + +flutter: diff --git a/plugins/aves_services_huawei/.gitignore b/plugins/aves_services_huawei/.gitignore new file mode 100644 index 000000000..9be145fde --- /dev/null +++ b/plugins/aves_services_huawei/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/plugins/aves_services_huawei/.metadata b/plugins/aves_services_huawei/.metadata new file mode 100644 index 000000000..c24d00d29 --- /dev/null +++ b/plugins/aves_services_huawei/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 5464c5bac742001448fe4fc0597be939379f88ea + channel: stable + +project_type: package diff --git a/plugins/aves_services_huawei/analysis_options.yaml b/plugins/aves_services_huawei/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/plugins/aves_services_huawei/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/plugins/aves_services_huawei/lib/aves_services_platform.dart b/plugins/aves_services_huawei/lib/aves_services_platform.dart new file mode 100644 index 000000000..6132349fd --- /dev/null +++ b/plugins/aves_services_huawei/lib/aves_services_platform.dart @@ -0,0 +1,75 @@ +library aves_services_platform; + +import 'package:aves_map/aves_map.dart'; +import 'package:aves_services/aves_services.dart'; +import 'package:aves_services_platform/src/map.dart'; +import 'package:flutter/widgets.dart'; +import 'package:huawei_hmsavailability/huawei_hmsavailability.dart'; +import 'package:latlong2/latlong.dart'; + +class PlatformMobileServices extends MobileServices { + // cf https://developer.huawei.com/consumer/en/doc/development/hmscore-common-References/huaweiapiavailability-0000001050121134#section9492524178 + static const int _hmsCoreAvailable = 0; + + bool _isAvailable = false; + + @override + Future init() async { + final result = await HmsApiAvailability().isHMSAvailable(); + _isAvailable = result == _hmsCoreAvailable; + debugPrint('Device has Huawei Mobile Services=$_isAvailable'); + } + + @override + bool get isServiceAvailable => _isAvailable; + + @override + EntryMapStyle get defaultMapStyle => EntryMapStyle.hmsNormal; + + @override + List get mapStyles => isServiceAvailable + ? [ + EntryMapStyle.hmsNormal, + EntryMapStyle.hmsTerrain, + ] + : []; + + @override + Widget buildMap({ + required AvesMapController? controller, + required Listenable clusterListenable, + required ValueNotifier boundsNotifier, + required EntryMapStyle style, + required TransitionBuilder decoratorBuilder, + required ButtonPanelBuilder buttonPanelBuilder, + required MarkerClusterBuilder markerClusterBuilder, + required MarkerWidgetBuilder markerWidgetBuilder, + required MarkerImageReadyChecker markerImageReadyChecker, + required ValueNotifier? dotLocationNotifier, + required ValueNotifier? overlayOpacityNotifier, + required MapOverlay? overlayEntry, + required UserZoomChangeCallback? onUserZoomChange, + required MapTapCallback? onMapTap, + required MarkerTapCallback? onMarkerTap, + }) { + return EntryHmsMap( + controller: controller, + clusterListenable: clusterListenable, + boundsNotifier: boundsNotifier, + minZoom: 3, + maxZoom: 20, + style: style, + decoratorBuilder: decoratorBuilder, + buttonPanelBuilder: buttonPanelBuilder, + markerClusterBuilder: markerClusterBuilder, + markerWidgetBuilder: markerWidgetBuilder, + markerImageReadyChecker: markerImageReadyChecker, + dotLocationNotifier: dotLocationNotifier, + overlayOpacityNotifier: overlayOpacityNotifier, + overlayEntry: overlayEntry, + onUserZoomChange: onUserZoomChange, + onMapTap: onMapTap, + onMarkerTap: onMarkerTap, + ); + } +} diff --git a/lib/widgets/common/map/google/map.dart b/plugins/aves_services_huawei/lib/src/map.dart similarity index 52% rename from lib/widgets/common/map/google/map.dart rename to plugins/aves_services_huawei/lib/src/map.dart index a5f9c620f..b7226c42f 100644 --- a/lib/widgets/common/map/google/map.dart +++ b/plugins/aves_services_huawei/lib/src/map.dart @@ -1,70 +1,61 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:aves/model/entry_images.dart'; -import 'package:aves/model/geotiff.dart'; -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/utils/change_notifier.dart'; -import 'package:aves/widgets/common/map/buttons.dart'; -import 'package:aves/widgets/common/map/controller.dart'; -import 'package:aves/widgets/common/map/decorator.dart'; -import 'package:aves/widgets/common/map/geo_entry.dart'; -import 'package:aves/widgets/common/map/geo_map.dart'; -import 'package:aves/widgets/common/map/google/geotiff_tile_provider.dart'; -import 'package:aves/widgets/common/map/google/marker_generator.dart'; -import 'package:aves/widgets/common/map/marker.dart'; -import 'package:aves/widgets/common/map/theme.dart'; -import 'package:aves/widgets/common/map/zoomed_bounds.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:huawei_map/map.dart'; import 'package:latlong2/latlong.dart' as ll; import 'package:provider/provider.dart'; -class EntryGoogleMap extends StatefulWidget { +class EntryHmsMap extends StatefulWidget { final AvesMapController? controller; final Listenable clusterListenable; final ValueNotifier boundsNotifier; final double? minZoom, maxZoom; final EntryMapStyle style; - final MarkerClusterBuilder markerClusterBuilder; - final MarkerWidgetBuilder markerWidgetBuilder; + final TransitionBuilder decoratorBuilder; + final ButtonPanelBuilder buttonPanelBuilder; + final MarkerClusterBuilder markerClusterBuilder; + final MarkerWidgetBuilder markerWidgetBuilder; + final MarkerImageReadyChecker markerImageReadyChecker; final ValueNotifier? dotLocationNotifier; final ValueNotifier? overlayOpacityNotifier; - final MappedGeoTiff? overlayEntry; + final MapOverlay? overlayEntry; final UserZoomChangeCallback? onUserZoomChange; - final void Function(ll.LatLng location)? onMapTap; - final void Function(GeoEntry geoEntry)? onMarkerTap; - final MapOpener? openMapPage; + final MapTapCallback? onMapTap; + final MarkerTapCallback? onMarkerTap; - const EntryGoogleMap({ - Key? key, + const EntryHmsMap({ + super.key, this.controller, required this.clusterListenable, required this.boundsNotifier, this.minZoom, this.maxZoom, required this.style, + required this.decoratorBuilder, + required this.buttonPanelBuilder, required this.markerClusterBuilder, required this.markerWidgetBuilder, + required this.markerImageReadyChecker, required this.dotLocationNotifier, this.overlayOpacityNotifier, this.overlayEntry, this.onUserZoomChange, this.onMapTap, this.onMarkerTap, - this.openMapPage, - }) : super(key: key); + }); @override - State createState() => _EntryGoogleMapState(); + State createState() => _EntryHmsMapState(); } -class _EntryGoogleMapState extends State with WidgetsBindingObserver { - GoogleMapController? _googleMapController; +class _EntryHmsMapState extends State> { + HuaweiMapController? _serviceMapController; final List _subscriptions = []; - Map _geoEntryByMarkerKey = {}; - final Map _markerBitmaps = {}; - final AChangeNotifier _markerBitmapChangeNotifier = AChangeNotifier(); + Map, GeoEntry> _geoEntryByMarkerKey = {}; + final Map, Uint8List> _markerBitmaps = {}; + final StreamController> _markerBitmapReadyStreamController = StreamController.broadcast(); Uint8List? _dotMarkerBitmap; ValueNotifier get boundsNotifier => widget.boundsNotifier; @@ -76,12 +67,11 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse @override void initState() { super.initState(); - WidgetsBinding.instance!.addObserver(this); _registerWidget(widget); } @override - void didUpdateWidget(covariant EntryGoogleMap oldWidget) { + void didUpdateWidget(covariant EntryHmsMap oldWidget) { super.didUpdateWidget(oldWidget); _unregisterWidget(oldWidget); _registerWidget(widget); @@ -90,41 +80,24 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse @override void dispose() { _unregisterWidget(widget); - _googleMapController?.dispose(); - WidgetsBinding.instance!.removeObserver(this); super.dispose(); } - void _registerWidget(EntryGoogleMap widget) { + void _registerWidget(EntryHmsMap widget) { final avesMapController = widget.controller; if (avesMapController != null) { - _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toGoogleLatLng(event.latLng)))); + _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng)))); } widget.clusterListenable.addListener(_updateMarkers); } - void _unregisterWidget(EntryGoogleMap widget) { + void _unregisterWidget(EntryHmsMap widget) { widget.clusterListenable.removeListener(_updateMarkers); _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); } - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - switch (state) { - case AppLifecycleState.inactive: - case AppLifecycleState.paused: - case AppLifecycleState.detached: - break; - case AppLifecycleState.resumed: - // workaround for blank Google map when resuming app - // cf https://github.com/flutter/flutter/issues/40284 - _googleMapController?.setMapStyle(null); - break; - } - } - @override Widget build(BuildContext context) { return Stack( @@ -134,31 +107,24 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse isReadyToRender: (key) => true, onRendered: (key, bitmap) => _dotMarkerBitmap = bitmap, ), - MarkerGeneratorWidget( + MarkerGeneratorWidget>( markers: _geoEntryByMarkerKey.keys.map(widget.markerWidgetBuilder).toList(), - isReadyToRender: (key) => key.entry.isThumbnailReady(extent: GeoMap.markerImageExtent), + isReadyToRender: widget.markerImageReadyChecker, onRendered: (key, bitmap) { _markerBitmaps[key] = bitmap; - _markerBitmapChangeNotifier.notify(); + _markerBitmapReadyStreamController.add(key); }, ), - MapDecorator( - child: _buildMap(), - ), - MapButtonPanel( - boundsNotifier: boundsNotifier, - zoomBy: _zoomBy, - openMapPage: widget.openMapPage, - resetRotation: _resetRotation, - ), + widget.decoratorBuilder(context, _buildMap()), + widget.buttonPanelBuilder(_zoomBy, _resetRotation), ], ); } Widget _buildMap() { - return AnimatedBuilder( - animation: _markerBitmapChangeNotifier, - builder: (context, child) { + return StreamBuilder( + stream: _markerBitmapReadyStreamController.stream, + builder: (context, _) { final markers = {}; _geoEntryByMarkerKey.forEach((markerKey, geoEntry) { final bytes = _markerBitmaps[markerKey]; @@ -166,76 +132,112 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse final point = LatLng(geoEntry.latitude!, geoEntry.longitude!); markers.add(Marker( markerId: MarkerId(geoEntry.markerId!), - consumeTapEvents: true, + clickable: true, icon: BitmapDescriptor.fromBytes(bytes), position: point, - onTap: () => widget.onMarkerTap?.call(geoEntry), + onClick: () => widget.onMarkerTap?.call(geoEntry), )); } }); final interactive = context.select((v) => v.interactive); - final overlayEntry = widget.overlayEntry; + // final overlayEntry = widget.overlayEntry; return ValueListenableBuilder( valueListenable: widget.dotLocationNotifier ?? ValueNotifier(null), builder: (context, dotLocation, child) { return ValueListenableBuilder( valueListenable: widget.overlayOpacityNotifier ?? ValueNotifier(1), builder: (context, overlayOpacity, child) { - return GoogleMap( + return HuaweiMap( initialCameraPosition: CameraPosition( - bearing: -bounds.rotation, - target: _toGoogleLatLng(bounds.projectedCenter), + bearing: bounds.rotation, + target: _toServiceLatLng(bounds.projectedCenter), zoom: bounds.zoom, ), - onMapCreated: (controller) async { - _googleMapController = controller; - final zoom = await controller.getZoomLevel(); - await _updateVisibleRegion(zoom: zoom, rotation: bounds.rotation); - if (mounted) { - setState(() {}); - } - }, + mapType: _toMapType(widget.style), // compass disabled to use provider agnostic controls compassEnabled: false, mapToolbarEnabled: false, - mapType: _toMapType(widget.style), - minMaxZoomPreference: MinMaxZoomPreference(widget.minZoom, widget.maxZoom), - rotateGesturesEnabled: true, + minMaxZoomPreference: MinMaxZoomPreference( + widget.minZoom ?? MinMaxZoomPreference.unbounded.minZoom, + widget.maxZoom ?? MinMaxZoomPreference.unbounded.maxZoom, + ), + // `allGesturesEnabled`, if defined overrides specific gesture settings + rotateGesturesEnabled: interactive, scrollGesturesEnabled: interactive, // zoom controls disabled to use provider agnostic controls zoomControlsEnabled: false, zoomGesturesEnabled: interactive, - // lite mode disabled because it lacks camera animation - liteModeEnabled: false, // tilt disabled to match leaflet tiltGesturesEnabled: false, myLocationEnabled: false, myLocationButtonEnabled: false, + trafficEnabled: false, + isScrollGesturesEnabledDuringRotateOrZoom: true, markers: { ...markers, if (dotLocation != null && _dotMarkerBitmap != null) Marker( - markerId: const MarkerId('dot'), + markerId: MarkerId('dot'), anchor: const Offset(.5, .5), - consumeTapEvents: true, + clickable: true, icon: BitmapDescriptor.fromBytes(_dotMarkerBitmap!), - position: _toGoogleLatLng(dotLocation), + position: _toServiceLatLng(dotLocation), zIndex: 1, ) }, - // TODO TLAD [geotiff] may use ground overlay instead when this is fixed: https://github.com/flutter/flutter/issues/26479 - tileOverlays: { - if (overlayEntry != null && overlayEntry.canOverlay) - TileOverlay( - tileOverlayId: TileOverlayId(overlayEntry.entry.uri), - tileProvider: GeoTiffTileProvider(overlayEntry), - transparency: 1 - overlayOpacity, - ), + // TODO TLAD [hms] GeoTIFF ground overlay + // groundOverlays: { + // if (overlayEntry != null && overlayEntry.canOverlay) + // GroundOverlay( + // groundOverlayId: GroundOverlayId('overlay'), + // // Google Maps API allows defining overlay either via + // // 1) position, anchor and width/height (in meters) + // // 2) bounds + // // Huawei requires width/height (in meters?), but also allows bounds... + // width: 42, + // height: 42, + // imageDescriptor: BitmapDescriptor.defaultMarker, + // position: _toServiceLatLng(overlayEntry.center!), + // ), + // }, + // TODO TLAD [hms] dynamic tile provider from current bounds, + // tileOverlays: { + // if (overlayEntry != null && overlayEntry.canOverlay) + // TileOverlay( + // tileOverlayId: TileOverlayId(overlayEntry.entry.uri), + // // `tileProvider` is `RepetitiveTile`, `UrlTile` or List + // // tileProvider: [ + // // Tile( + // // x: x, + // // y: y, + // // zoom: zoom, + // // imageData: imageData, + // // ), + // // ], + // transparency: 1 - overlayOpacity, + // ), + // }, + onMapCreated: (controller) async { + _serviceMapController = controller; + final zoom = await controller.getZoomLevel(); + await _updateVisibleRegion(zoom: zoom ?? bounds.zoom, rotation: bounds.rotation); + if (mounted) { + setState(() {}); + } }, - onCameraMove: (position) => _updateVisibleRegion(zoom: position.zoom, rotation: -position.bearing), + onCameraMove: (position) => _updateVisibleRegion(zoom: position.zoom, rotation: position.bearing), onCameraIdle: _onIdle, - onTap: (position) => widget.onMapTap?.call(_fromGoogleLatLng(position)), + onClick: (position) => widget.onMapTap?.call(_fromServiceLatLng(position)), + onPoiClick: (poi) { + final poiPosition = poi.latLng; + if (poiPosition != null) { + widget.onMapTap?.call(_fromServiceLatLng(poiPosition)); + } + }, + logoPadding: const EdgeInsets.all(8), + // lite mode disabled because it is not interactive + liteMode: false, ); }, ); @@ -258,62 +260,63 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse Future _updateVisibleRegion({required double zoom, required double rotation}) async { if (!mounted) return; - final bounds = await _googleMapController?.getVisibleRegion(); + final bounds = await _serviceMapController?.getVisibleRegion(); if (bounds != null && (bounds.northeast != uninitializedLatLng || bounds.southwest != uninitializedLatLng)) { final sw = bounds.southwest; final ne = bounds.northeast; boundsNotifier.value = ZoomedBounds( - sw: _fromGoogleLatLng(sw), - ne: _fromGoogleLatLng(ne), + sw: _fromServiceLatLng(sw), + ne: _fromServiceLatLng(ne), zoom: zoom, rotation: rotation, ); } else { // the visible region is sometimes uninitialized when queried right after creation, // so we query it again next frame - WidgetsBinding.instance!.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { _updateVisibleRegion(zoom: zoom, rotation: rotation); }); } } Future _resetRotation() async { - final controller = _googleMapController; + final controller = _serviceMapController; if (controller == null) return; await controller.animateCamera(CameraUpdate.newCameraPosition(CameraPosition( - target: _toGoogleLatLng(bounds.projectedCenter), + target: _toServiceLatLng(bounds.projectedCenter), zoom: bounds.zoom, ))); } Future _zoomBy(double amount) async { - final controller = _googleMapController; + final controller = _serviceMapController; if (controller == null) return; - widget.onUserZoomChange?.call(await controller.getZoomLevel() + amount); + final zoom = await controller.getZoomLevel(); + if (zoom == null) return; + + widget.onUserZoomChange?.call(zoom + amount); await controller.animateCamera(CameraUpdate.zoomBy(amount)); } Future _moveTo(LatLng point) async { - final controller = _googleMapController; + final controller = _serviceMapController; if (controller == null) return; await controller.animateCamera(CameraUpdate.newLatLng(point)); } // `LatLng` used by `google_maps_flutter` is not the one from `latlong2` package - LatLng _toGoogleLatLng(ll.LatLng location) => LatLng(location.latitude, location.longitude); + LatLng _toServiceLatLng(ll.LatLng location) => LatLng(location.latitude, location.longitude); - ll.LatLng _fromGoogleLatLng(LatLng location) => ll.LatLng(location.latitude, location.longitude); + ll.LatLng _fromServiceLatLng(LatLng location) => ll.LatLng(location.lat, location.lng); MapType _toMapType(EntryMapStyle style) { switch (style) { - case EntryMapStyle.googleNormal: + case EntryMapStyle.hmsNormal: return MapType.normal; - case EntryMapStyle.googleHybrid: - return MapType.hybrid; - case EntryMapStyle.googleTerrain: + case EntryMapStyle.hmsTerrain: return MapType.terrain; default: return MapType.none; diff --git a/plugins/aves_services_huawei/pubspec.yaml b/plugins/aves_services_huawei/pubspec.yaml new file mode 100644 index 000000000..4d2348621 --- /dev/null +++ b/plugins/aves_services_huawei/pubspec.yaml @@ -0,0 +1,27 @@ +name: aves_services_platform +version: 0.0.1 +publish_to: none + +environment: + sdk: ">=2.17.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + aves_map: + path: ../aves_map + aves_services: + path: ../aves_services + huawei_hmsavailability: + huawei_map: + git: + url: https://github.com/deckerst/hms-flutter-plugin.git + path: flutter-hms-map + ref: aves + latlong2: + provider: + +dev_dependencies: + flutter_lints: + +flutter: diff --git a/pubspec.lock b/pubspec.lock index 5d58ce3c9..81ed62b51 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,28 +7,28 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "31.0.0" + version: "40.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "2.8.0" + version: "4.1.0" archive: dependency: transitive description: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.1.6" + version: "3.1.11" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.3.1" async: dependency: transitive description: @@ -36,6 +36,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + aves_map: + dependency: "direct main" + description: + path: "plugins/aves_map" + relative: true + source: path + version: "0.0.1" aves_report: dependency: "direct main" description: @@ -50,13 +57,27 @@ packages: relative: true source: path version: "0.0.1" + aves_services: + dependency: "direct main" + description: + path: "plugins/aves_services" + relative: true + source: path + version: "0.0.1" + aves_services_platform: + dependency: "direct main" + description: + path: "plugins/aves_services_google" + relative: true + source: path + version: "0.0.1" barcode: dependency: transitive description: name: barcode url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.2.1" boolean_selector: dependency: transitive description: @@ -92,13 +113,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.0" - cli_util: - dependency: transitive - description: - name: cli_util - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.5" clock: dependency: transitive description: @@ -112,14 +126,14 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" connectivity_plus: dependency: "direct main" description: name: connectivity_plus url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.3.0" connectivity_plus_linux: dependency: transitive description: @@ -133,7 +147,7 @@ packages: name: connectivity_plus_macos url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.2" connectivity_plus_platform_interface: dependency: transitive description: @@ -175,7 +189,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.2.0" crypto: dependency: transitive description: @@ -184,7 +198,7 @@ packages: source: hosted version: "3.0.1" custom_rounded_rectangle_border: - dependency: "direct main" + dependency: transitive description: name: custom_rounded_rectangle_border url: "https://pub.dartlang.org" @@ -196,7 +210,7 @@ packages: name: dbus url: "https://pub.dartlang.org" source: hosted - version: "0.7.2" + version: "0.7.3" decorated_icon: dependency: "direct main" description: @@ -210,7 +224,7 @@ packages: name: device_info_plus url: "https://pub.dartlang.org" source: hosted - version: "3.2.2" + version: "3.2.3" device_info_plus_linux: dependency: transitive description: @@ -224,7 +238,7 @@ packages: name: device_info_plus_macos url: "https://pub.dartlang.org" source: hosted - version: "2.2.2" + version: "2.2.3" device_info_plus_platform_interface: dependency: transitive description: @@ -275,14 +289,14 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" ffi: dependency: transitive description: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.2.1" fijkplayer: dependency: "direct main" description: @@ -305,42 +319,42 @@ packages: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "1.13.1" + version: "1.17.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.2.5" + version: "4.4.0" firebase_core_web: dependency: transitive description: name: firebase_core_web url: "https://pub.dartlang.org" source: hosted - version: "1.6.1" + version: "1.6.4" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics url: "https://pub.dartlang.org" source: hosted - version: "2.5.3" + version: "2.8.0" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.2.1" + version: "3.2.6" flex_color_picker: dependency: "direct main" description: name: flex_color_picker url: "https://pub.dartlang.org" source: hosted - version: "2.3.1" + version: "2.5.0" fluster: dependency: "direct main" description: @@ -366,7 +380,7 @@ packages: name: flutter_displaymode url: "https://pub.dartlang.org" source: hosted - version: "0.3.2" + version: "0.4.0" flutter_driver: dependency: "direct dev" description: flutter @@ -385,7 +399,7 @@ packages: name: flutter_lints url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.1" flutter_localizations: dependency: "direct main" description: flutter @@ -404,14 +418,14 @@ packages: name: flutter_markdown url: "https://pub.dartlang.org" source: hosted - version: "0.6.9+1" + version: "0.6.10+1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" flutter_staggered_animations: dependency: "direct main" description: @@ -435,7 +449,7 @@ packages: name: frontend_server_client url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -456,26 +470,26 @@ packages: source: hosted version: "2.0.2" google_api_availability: - dependency: "direct main" + dependency: transitive description: name: google_api_availability url: "https://pub.dartlang.org" source: hosted version: "3.0.1" google_maps_flutter: - dependency: "direct main" + dependency: transitive description: name: google_maps_flutter url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.6" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "2.1.7" highlight: dependency: transitive description: @@ -503,7 +517,7 @@ packages: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.0.1" image: dependency: transitive description: @@ -531,7 +545,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" latlong2: dependency: "direct main" description: @@ -545,7 +559,7 @@ packages: name: lints url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "2.0.0" lists: dependency: transitive description: @@ -566,7 +580,7 @@ packages: name: markdown url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "5.0.0" matcher: dependency: transitive description: @@ -580,7 +594,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" material_design_icons_flutter: dependency: "direct main" description: @@ -608,7 +622,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" motion_sensors: dependency: transitive description: @@ -643,7 +657,7 @@ packages: name: overlay_support url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "2.0.0" package_config: dependency: transitive description: @@ -657,14 +671,14 @@ packages: name: package_info_plus url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.4.2" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.5" package_info_plus_macos: dependency: transitive description: @@ -685,14 +699,14 @@ packages: name: package_info_plus_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.5" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.5" palette_generator: dependency: "direct main" description: @@ -708,12 +722,12 @@ packages: source: hosted version: "0.4.0" path: - dependency: transitive + dependency: "direct main" description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" path_parsing: dependency: transitive description: @@ -727,35 +741,35 @@ packages: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "2.1.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" pdf: dependency: "direct main" description: name: pdf url: "https://pub.dartlang.org" source: hosted - version: "3.7.3" + version: "3.8.1" percent_indicator: dependency: "direct main" description: name: percent_indicator url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.2.2" permission_handler: dependency: "direct main" description: @@ -776,7 +790,7 @@ packages: name: permission_handler_apple url: "https://pub.dartlang.org" source: hosted - version: "9.0.3" + version: "9.0.4" permission_handler_platform_interface: dependency: transitive description: @@ -797,7 +811,7 @@ packages: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "5.0.0" platform: dependency: transitive description: @@ -832,7 +846,7 @@ packages: name: printing url: "https://pub.dartlang.org" source: hosted - version: "5.7.4" + version: "5.9.1" process: dependency: transitive description: @@ -853,7 +867,7 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "6.0.2" + version: "6.0.3" pub_semver: dependency: transitive description: @@ -867,49 +881,56 @@ packages: name: qr url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "3.0.1+1" + version: "3.1.0" screen_brightness: dependency: "direct main" description: name: screen_brightness url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.2.1" screen_brightness_android: dependency: transitive description: name: screen_brightness_android url: "https://pub.dartlang.org" source: hosted - version: "0.0.4" + version: "0.1.0" screen_brightness_ios: dependency: transitive description: name: screen_brightness_ios url: "https://pub.dartlang.org" source: hosted - version: "0.0.5" + version: "0.1.0" + screen_brightness_macos: + dependency: transitive + description: + name: screen_brightness_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0+1" screen_brightness_platform_interface: dependency: transitive description: name: screen_brightness_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "0.0.4" + version: "0.1.0" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.13" + version: "2.0.15" shared_preferences_android: dependency: "direct main" description: @@ -923,21 +944,21 @@ packages: name: shared_preferences_ios url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: @@ -951,21 +972,21 @@ packages: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" shelf_packages_handler: dependency: transitive description: @@ -1012,21 +1033,21 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" sqflite: dependency: "direct main" description: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.2+1" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.2.1+1" stack_trace: dependency: transitive description: @@ -1053,7 +1074,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: cd5ccd925d0348218aaf156f0b9dc4f8caaec7cc + resolved-ref: b8ad46de0322b3b107cb411dfbf373878692e657 url: "https://github.com/deckerst/aves_streams_channel.git" source: git version: "0.3.0" @@ -1091,21 +1112,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.19.5" + version: "1.21.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.13" transparent_image: dependency: "direct main" description: @@ -1140,35 +1161,35 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.20" + version: "6.1.2" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.15" + version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios url: "https://pub.dartlang.org" source: hosted - version: "6.0.15" + version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: @@ -1182,28 +1203,28 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.0.11" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" vm_service: dependency: transitive description: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "7.5.0" + version: "8.2.2" watcher: dependency: transitive description: @@ -1217,7 +1238,7 @@ packages: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0" webdriver: dependency: transitive description: @@ -1231,14 +1252,14 @@ packages: name: webkit_inspection_protocol url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.0" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.6.1" wkt_parser: dependency: transitive description: @@ -1259,14 +1280,14 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.3.1" + version: "5.4.1" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "3.1.1" sdks: - dart: ">=2.16.0 <3.0.0" - flutter: ">=2.10.0" + dart: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index e09f2bcf1..d920698e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,11 +6,11 @@ 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.6.4+70 +version: 1.6.5+71 publish_to: none environment: - sdk: '>=2.16.0 <3.0.0' + sdk: ">=2.17.0 <3.0.0" # following https://github.blog/2021-09-01-improving-git-protocol-security-github/ # dependency GitHub repos should be referenced via `https://`, not `git://` @@ -21,16 +21,20 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter + aves_map: + path: plugins/aves_map aves_report: path: plugins/aves_report aves_report_platform: path: plugins/aves_report_crashlytics + aves_services: + path: plugins/aves_services + aves_services_platform: + path: plugins/aves_services_google charts_flutter: collection: connectivity_plus: country_code: -# TODO TLAD as of 2022/02/22, null safe version is pre-release - custom_rounded_rectangle_border: '>=0.2.0-nullsafety.0' decorated_icon: device_info_plus: equatable: @@ -50,8 +54,6 @@ dependencies: flutter_markdown: flutter_staggered_animations: get_it: - google_api_availability: - google_maps_flutter: intl: latlong2: material_design_icons_flutter: @@ -60,6 +62,7 @@ dependencies: palette_generator: # TODO TLAD as of 2022/02/22, latest version (v0.4.1) has this issue: https://github.com/zesage/panorama/issues/25 panorama: 0.4.0 + path: pdf: percent_indicator: permission_handler: @@ -143,6 +146,9 @@ flutter: # `OutputBuffer` in `/services/common/output_buffer.dart` # adapts from Flutter `_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` +# # `EagerScaleGestureRecognizer` in `/widgets/common/behaviour/eager_scale_gesture_recognizer.dart` # adapts from Flutter `ScaleGestureRecognizer` in `/gestures/scale.dart` # diff --git a/scripts/apply_flavor_huawei.sh b/scripts/apply_flavor_huawei.sh new file mode 100755 index 000000000..6a89cf577 --- /dev/null +++ b/scripts/apply_flavor_huawei.sh @@ -0,0 +1,9 @@ +#!/bin/bash +PUBSPEC_PATH="../pubspec.yaml" + +flutter clean + +sed -i 's/aves_services_google/aves_services_huawei/g' "$PUBSPEC_PATH" +sed -i 's/aves_report_crashlytics/aves_report_console/g' "$PUBSPEC_PATH" + +flutter pub get diff --git a/scripts/apply_flavor_izzy.sh b/scripts/apply_flavor_izzy.sh index 598f41175..31af6b867 100755 --- a/scripts/apply_flavor_izzy.sh +++ b/scripts/apply_flavor_izzy.sh @@ -3,6 +3,7 @@ PUBSPEC_PATH="../pubspec.yaml" flutter clean +sed -i 's/aves_services_huawei/aves_services_google/g' "$PUBSPEC_PATH" sed -i 's/aves_report_crashlytics/aves_report_console/g' "$PUBSPEC_PATH" flutter pub get diff --git a/scripts/apply_flavor_play.sh b/scripts/apply_flavor_play.sh index d613cc879..a02b9a00d 100755 --- a/scripts/apply_flavor_play.sh +++ b/scripts/apply_flavor_play.sh @@ -3,6 +3,7 @@ PUBSPEC_PATH="../pubspec.yaml" flutter clean +sed -i 's/aves_services_huawei/aves_services_google/g' "$PUBSPEC_PATH" sed -i 's/aves_report_console/aves_report_crashlytics/g' "$PUBSPEC_PATH" flutter pub get diff --git a/scripts/pub_get_all.sh b/scripts/pub_get_all.sh new file mode 100755 index 000000000..9d9755d80 --- /dev/null +++ b/scripts/pub_get_all.sh @@ -0,0 +1,10 @@ +#!/bin/bash +cd .. +flutter pub get +cd plugins +for plugin in $(ls -d *); do + cd $plugin + flutter pub get + cd .. +done + diff --git a/scripts/pub_upgrade_all.sh b/scripts/pub_upgrade_all.sh new file mode 100755 index 000000000..b744db615 --- /dev/null +++ b/scripts/pub_upgrade_all.sh @@ -0,0 +1,10 @@ +#!/bin/bash +cd .. +flutter pub upgrade +cd plugins +for plugin in $(ls -d *); do + cd $plugin + flutter pub upgrade + cd .. +done + diff --git a/scripts/screenshot_post_process.sh b/scripts/screenshot_post_process.sh index 45586f5de..0a1db1281 100755 --- a/scripts/screenshot_post_process.sh +++ b/scripts/screenshot_post_process.sh @@ -37,6 +37,10 @@ for source in overlay/*/*; do convert -resize 350x "$source" "$target" fi done +mv screenshots/izzy/en screenshots/izzy/en-US +mv screenshots/izzy/es screenshots/izzy/es-MX +mv screenshots/izzy/pt screenshots/izzy/pt-BR +mv screenshots/izzy/zh screenshots/izzy/zh-CN # play: add device frame for source in overlay/*/*; do @@ -67,3 +71,13 @@ for source in framed/en/*; do convert -resize 250x "$source" "$target" fi done + +# amazon: scale down +for source in framed/en/*; do + if [[ -f "$source" ]]; then + target=${source/framed/amazon} + echo "$source -> $target" + mkdir -p "$(dirname "$target")" + convert -resize x1920 "$source" -gravity center -background transparent -extent 1200x1920 "$target" + fi +done diff --git a/shaders_2.10.4.sksl.json b/shaders_2.10.4.sksl.json deleted file mode 100644 index c02275894..000000000 --- a/shaders_2.10.4.sksl.json +++ /dev/null @@ -1 +0,0 @@ -{"platform":"android","name":"SM G970N","engineRevision":"57d3bac3dd5cb5b0e464ab70e7bc8a0d8cf083ab","data":{"HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAEQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABPAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACtBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAADAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAYAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIA":"CAAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAOMGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgxKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IENsYW1wZWRHcmFkaWVudF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF81X2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABZQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACnAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABGAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAACEA2X4PLOGEAAAAAAAAACAAAAAVQQAAQAAAAAQCDIBCAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAAAAAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAACwQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBDaXJjbGVCbHVyX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TMS54eSkpICogZmxvYXQodWNpcmNsZURhdGFfUzEudykpOwoJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfUzEueikgKiB1Y2lyY2xlRGF0YV9TMS53OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIE1hdHJpeEVmZmVjdF9TMV9jMChfdG1wXzBfaW5Db2xvciwgZmxvYXQyKGhhbGYyKGRpc3QsIDAuNSkpKS53KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZUJsdXJfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","B2AAQAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CAAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cgl2aW5Db3ZlcmFnZV9TMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAZQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBoYWxmIHZpbkNvdmVyYWdlX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdUNvbG9yX1MwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIBSQB5VRECGAEAAAMAAAAAAAAAAACAA4AAAACAAAAAAACCAYAA":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAA4wQAAGNvbnN0IGludCBrRmlsbEJXX1MxID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGNvdmVyYWdlOwoJaWYgKGludCgxKSA9PSBrRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fUzEuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1MxLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7Cgl9CgllbHNlIAoJewoJCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1MxKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCWlmIChpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIGNvdmVyYWdlKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAAAQAAAAGQCBAMQACAAAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAIgDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzAueCwgdWNsYW1wX1MxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIAAIAAAABLCIABAAAAABAEGABBAMAACAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAdBAAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZC54ID0gY2xhbXAoc3Vic2V0Q29vcmQueCwgdWNsYW1wX1MxX2MwX2MwLngsIHVjbGFtcF9TMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gc3Vic2V0Q29vcmQueTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQmxlbmRfUzEoaGFsZjQgX3NyYywgaGFsZjQgX2RzdCkgCnsKCS8vIEJsZW5kIG1vZGU6IE1vZHVsYXRlCglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoTWF0cml4RWZmZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvbG9yX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","DASAAAAAQAAWAABAYAAQBYH7777Z6QQBAEAAAAAAEAAAAAAAEBSAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1PVAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc1NpemVJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAD4AQAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfUzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHVDb2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCk7Cgl9CglvdXRwdXRDb2xvcl9TMCA9IG91dHB1dENvbG9yX1MwICogdGV4Q29sb3I7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAB3QA6AAAEAAAAAAAMAAPEAEAAABAAAAAAB2AAAAAAACAAAAAEBSAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAA2BQAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzI7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MyOwppbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglhbHBoYSA9IDEuMCAtIGFscGhhOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CmhhbGY0IENpcmN1bGFyUlJlY3RfUzIoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MyLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MyLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzIueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCWhhbGY0IG91dHB1dF9TMjsKCW91dHB1dF9TMiA9IENpcmN1bGFyUlJlY3RfUzIob3V0cHV0X1MxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMjsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HTQAAGAABBYAAAEIXBAAAGEAMAAAAAAAAAAAAAAAQAHAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1M/AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgABAAAAHQMAAGluIGZsb2F0NCB2UXVhZEVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGYgZWRnZUFscGhhOwoJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1MwLnosIHZRdWFkRWRnZV9TMC53KSArIDAuNSwgMS4wKSk7Cgl9CgllbHNlIAoJewoJCWhhbGYyIGdGID0gaGFsZjIoaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TMC54KnZRdWFkRWRnZV9TMC54IC0gdlF1YWRFZGdlX1MwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAKAAAAaW5RdWFkRWRnZQAAAQAAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAEDAACATAAABAGYAAAICSBYQCA4AAAAAAAA5AAAAAAABAAAAACAZAAAAA":"CAAAAExTS1OxCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmFyY2Nvb3JkX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0IGFhX2Jsb2F0X211bHRpcGxpZXIgPSAxOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgaXNfbGluZWFyX2NvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnc7CglmbG9hdDIgcGl4ZWxsZW5ndGggPSBpbnZlcnNlc3FydChmbG9hdDIoZG90KHNrZXcueHosIHNrZXcueHopLCBkb3Qoc2tldy55dywgc2tldy55dykpKTsKCWZsb2F0NCBub3JtYWxpemVkX2F4aXNfZGlycyA9IHNrZXcgKiBwaXhlbGxlbmd0aC54eXh5OwoJZmxvYXQyIGF4aXN3aWR0aHMgPSAoYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnh5KSArIGFicyhub3JtYWxpemVkX2F4aXNfZGlycy56dykpOwoJZmxvYXQyIGFhX2Jsb2F0cmFkaXVzID0gYXhpc3dpZHRocyAqIHBpeGVsbGVuZ3RoICogLjU7CglmbG9hdDQgcmFkaWlfYW5kX25laWdoYm9ycyA9IHJhZGlpX3NlbGVjdG9yKiBmbG9hdDR4NChyYWRpaV94LCByYWRpaV95LCByYWRpaV94Lnl4d3osIHJhZGlpX3kud3p5eCk7CglmbG9hdDIgcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnh5OwoJZmxvYXQyIG5laWdoYm9yX3JhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy56dzsKCWZsb2F0IGNvdmVyYWdlX211bHRpcGxpZXIgPSAxOwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlX211bHRpcGxpZXIgPSAxIC8gKG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKSk7CgkJcmFkaWkgPSBmbG9hdDIoMCk7Cgl9CglmbG9hdCBjb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS56OwoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjUpKSkgCgl7CgkJcmFkaWkgPSBmbG9hdDIoMCk7CgkJYWFfYmxvYXRfZGlyZWN0aW9uID0gc2lnbihjb3JuZXIpOwoJCWlmIChjb3ZlcmFnZSA+IC41KSAKCQl7CgkJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IC1hYV9ibG9hdF9kaXJlY3Rpb247CgkJfQoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24gKiBhYV9ibG9hdHJhZGl1cyAqIGFhX2Jsb2F0X211bHRpcGxpZXI7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJaWYgKGNvdmVyYWdlID4gLjUpIAoJewoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueCAhPSAwICYmIHZlcnRleHBvcy54ICogY29ybmVyLnggPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLngpOwoJCQl2ZXJ0ZXhwb3MueCA9IDA7CgkJCXZlcnRleHBvcy55ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci55KSAqIHBpeGVsbGVuZ3RoLnkvcGl4ZWxsZW5ndGgueDsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLngpIC8gKGFicyhjb3JuZXIueCkgKyBiYWNrc2V0KSArIC41OwoJCX0KCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnkgIT0gMCAmJiB2ZXJ0ZXhwb3MueSAqIGNvcm5lci55IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy55KTsKCQkJdmVydGV4cG9zLnkgPSAwOwoJCQl2ZXJ0ZXhwb3MueCArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueCkgKiBwaXhlbGxlbmd0aC54L3BpeGVsbGVuZ3RoLnk7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci55KSAvIChhYnMoY29ybmVyLnkpICsgYmFja3NldCkgKyAuNTsKCQl9Cgl9CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoYXJjY29vcmQueCsxLCBhcmNjb29yZC55KTsKCX0KCXNrX1Bvc2l0aW9uID0gZGV2Y29vcmQueHkwMTsKfQoAAAAAAAAAXQIAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IHhfcGx1c18xPXZhcmNjb29yZF9TMC54LCB5PXZhcmNjb29yZF9TMC55OwoJaGFsZiBjb3ZlcmFnZTsKCWlmICgwID09IHhfcGx1c18xKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoeSk7Cgl9CgllbHNlIAoJewoJCWZsb2F0IGZuID0geF9wbHVzXzEgKiAoeF9wbHVzXzEgLSAyKTsKCQlmbiA9IGZtYSh5LHksIGZuKTsKCQlmbG9hdCBmbndpZHRoID0gZndpZHRoKGZuKTsKCQljb3ZlcmFnZSA9IC41IC0gaGFsZihmbi9mbndpZHRoKTsKCQljb3ZlcmFnZSA9IGNsYW1wKGNvdmVyYWdlLCAwLCAxKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoY292ZXJhZ2UpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAEDAACATAAABAGYAAAICSBYQCA4AAAAAAEADZABYAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CAAAAExTS1OxCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmFyY2Nvb3JkX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0IGFhX2Jsb2F0X211bHRpcGxpZXIgPSAxOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgaXNfbGluZWFyX2NvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnc7CglmbG9hdDIgcGl4ZWxsZW5ndGggPSBpbnZlcnNlc3FydChmbG9hdDIoZG90KHNrZXcueHosIHNrZXcueHopLCBkb3Qoc2tldy55dywgc2tldy55dykpKTsKCWZsb2F0NCBub3JtYWxpemVkX2F4aXNfZGlycyA9IHNrZXcgKiBwaXhlbGxlbmd0aC54eXh5OwoJZmxvYXQyIGF4aXN3aWR0aHMgPSAoYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnh5KSArIGFicyhub3JtYWxpemVkX2F4aXNfZGlycy56dykpOwoJZmxvYXQyIGFhX2Jsb2F0cmFkaXVzID0gYXhpc3dpZHRocyAqIHBpeGVsbGVuZ3RoICogLjU7CglmbG9hdDQgcmFkaWlfYW5kX25laWdoYm9ycyA9IHJhZGlpX3NlbGVjdG9yKiBmbG9hdDR4NChyYWRpaV94LCByYWRpaV95LCByYWRpaV94Lnl4d3osIHJhZGlpX3kud3p5eCk7CglmbG9hdDIgcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnh5OwoJZmxvYXQyIG5laWdoYm9yX3JhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy56dzsKCWZsb2F0IGNvdmVyYWdlX211bHRpcGxpZXIgPSAxOwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlX211bHRpcGxpZXIgPSAxIC8gKG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKSk7CgkJcmFkaWkgPSBmbG9hdDIoMCk7Cgl9CglmbG9hdCBjb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS56OwoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjUpKSkgCgl7CgkJcmFkaWkgPSBmbG9hdDIoMCk7CgkJYWFfYmxvYXRfZGlyZWN0aW9uID0gc2lnbihjb3JuZXIpOwoJCWlmIChjb3ZlcmFnZSA+IC41KSAKCQl7CgkJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IC1hYV9ibG9hdF9kaXJlY3Rpb247CgkJfQoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24gKiBhYV9ibG9hdHJhZGl1cyAqIGFhX2Jsb2F0X211bHRpcGxpZXI7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJaWYgKGNvdmVyYWdlID4gLjUpIAoJewoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueCAhPSAwICYmIHZlcnRleHBvcy54ICogY29ybmVyLnggPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLngpOwoJCQl2ZXJ0ZXhwb3MueCA9IDA7CgkJCXZlcnRleHBvcy55ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci55KSAqIHBpeGVsbGVuZ3RoLnkvcGl4ZWxsZW5ndGgueDsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLngpIC8gKGFicyhjb3JuZXIueCkgKyBiYWNrc2V0KSArIC41OwoJCX0KCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnkgIT0gMCAmJiB2ZXJ0ZXhwb3MueSAqIGNvcm5lci55IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy55KTsKCQkJdmVydGV4cG9zLnkgPSAwOwoJCQl2ZXJ0ZXhwb3MueCArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueCkgKiBwaXhlbGxlbmd0aC54L3BpeGVsbGVuZ3RoLnk7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci55KSAvIChhYnMoY29ybmVyLnkpICsgYmFja3NldCkgKyAuNTsKCQl9Cgl9CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoYXJjY29vcmQueCsxLCBhcmNjb29yZC55KTsKCX0KCXNrX1Bvc2l0aW9uID0gZGV2Y29vcmQueHkwMTsKfQoAAAABAAAA7AMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IHhfcGx1c18xPXZhcmNjb29yZF9TMC54LCB5PXZhcmNjb29yZF9TMC55OwoJaGFsZiBjb3ZlcmFnZTsKCWlmICgwID09IHhfcGx1c18xKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoeSk7Cgl9CgllbHNlIAoJewoJCWZsb2F0IGZuID0geF9wbHVzXzEgKiAoeF9wbHVzXzEgLSAyKTsKCQlmbiA9IGZtYSh5LHksIGZuKTsKCQlmbG9hdCBmbndpZHRoID0gZndpZHRoKGZuKTsKCQljb3ZlcmFnZSA9IC41IC0gaGFsZihmbi9mbndpZHRoKTsKCQljb3ZlcmFnZSA9IGNsYW1wKGNvdmVyYWdlLCAwLCAxKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoY292ZXJhZ2UpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","GEMAAAYAAEHAAAARC4EAAAQWBQAAAAAAAAAQAAAAIBCAAAGQAEAAAAAQAAAABAEQAEAAAAA":"CAAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAjAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA4AAABpblNoYWRvd1BhcmFtcwAAAQAAAAAAAAA=","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAABAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAIAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIA":"CAAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAOMGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgwKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IENsYW1wZWRHcmFkaWVudF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF81X2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAAcBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","AYTRVAADQAAAOAEARAFQJAABBADAAAILBYAACCYUQD777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7CmluIGhhbGYzIGluQ2xpcFBsYW5lOwppbiBoYWxmMyBpbklzZWN0UGxhbmU7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKb3V0IGhhbGYzIHZpbklzZWN0UGxhbmVfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5DbGlwUGxhbmVfUzAgPSBpbkNsaXBQbGFuZTsKCXZpbklzZWN0UGxhbmVfUzAgPSBpbklzZWN0UGxhbmU7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1MwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TMC55dzsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAD1AwAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKaW4gaGFsZjMgdmluSXNlY3RQbGFuZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGYzIGNsaXBQbGFuZTsKCWNsaXBQbGFuZSA9IHZpbkNsaXBQbGFuZV9TMDsKCWhhbGYzIGlzZWN0UGxhbmU7Cglpc2VjdFBsYW5lID0gdmluSXNlY3RQbGFuZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGYgY2xpcCA9IGhhbGYoc2F0dXJhdGUoY2lyY2xlRWRnZS56ICogZG90KGNpcmNsZUVkZ2UueHksIGNsaXBQbGFuZS54eSkgKyBjbGlwUGxhbmUueikpOwoJY2xpcCAqPSBoYWxmKHNhdHVyYXRlKGNpcmNsZUVkZ2UueiAqIGRvdChjaXJjbGVFZGdlLnh5LCBpc2VjdFBsYW5lLnh5KSArIGlzZWN0UGxhbmUueikpOwoJZWRnZUFscGhhICo9IGNsaXA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAFAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2ULAAAAaW5DbGlwUGxhbmUADAAAAGluSXNlY3RQbGFuZQEAAAAAAAAA","EABQAAAAAEAAAAAQAABQAAIOAAABCFYIAAKAUDAAAAAAAAABAAAAAAAAAAANAAIAAAABAAAAACAJAAIAAAAA":"CAAAAExTS1OhAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgZmxvYXQyIHZJbnRUZXh0dXJlQ29vcmRzX1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERpc3RhbmNlRmllbGRQYXRoCglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc0RpbWVuc2lvbnNJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJdkludFRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkczsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8yX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEaXN0YW5jZUZpZWxkUGF0aAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfUzA7CgloYWxmNCB0ZXhDb2xvcjsKCXsKCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLnJycnI7Cgl9CgloYWxmIGRpc3RhbmNlID0gNy45Njg3NSoodGV4Q29sb3IuciAtIDAuNTAxOTYwNzg0MzEpOwoJaGFsZiBhZndpZHRoOwoJYWZ3aWR0aCA9IGFicygwLjY1KmhhbGYoZEZkeCh2SW50VGV4dHVyZUNvb3Jkc19TMC54KSkpOwoJaGFsZiB2YWwgPSBzbW9vdGhzdGVwKC1hZndpZHRoLCBhZndpZHRoLCBkaXN0YW5jZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KHZhbCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","HUJBYAQCAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGACQAGAAAAAQAAAAAAAQQGAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAAAPBgAAdW5pZm9ybSBoYWxmIHVTcmNURl9TMFs3XTsKdW5pZm9ybSBoYWxmM3gzIHVDb2xvclhmb3JtX1MwOwp1bmlmb3JtIGhhbGYgdURzdFRGX1MwWzddOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmIHNyY190Zl9TMChoYWxmIHgpIAp7CgloYWxmIEcgPSB1U3JjVEZfUzBbMF07CgloYWxmIEEgPSB1U3JjVEZfUzBbMV07CgloYWxmIEIgPSB1U3JjVEZfUzBbMl07CgloYWxmIEMgPSB1U3JjVEZfUzBbM107CgloYWxmIEQgPSB1U3JjVEZfUzBbNF07CgloYWxmIEUgPSB1U3JjVEZfUzBbNV07CgloYWxmIEYgPSB1U3JjVEZfUzBbNl07CgloYWxmIHMgPSBzaWduKHgpOwoJeCA9IGFicyh4KTsKCXggPSAoeCA8IEQpID8gKEMgKiB4KSArIEYgOiBwb3coQSAqIHggKyBCLCBHKSArIEU7CglyZXR1cm4gcyAqIHg7Cn0KaGFsZiBkc3RfdGZfUzAoaGFsZiB4KSAKewoJaGFsZiBHID0gdURzdFRGX1MwWzBdOwoJaGFsZiBBID0gdURzdFRGX1MwWzFdOwoJaGFsZiBCID0gdURzdFRGX1MwWzJdOwoJaGFsZiBDID0gdURzdFRGX1MwWzNdOwoJaGFsZiBEID0gdURzdFRGX1MwWzRdOwoJaGFsZiBFID0gdURzdFRGX1MwWzVdOwoJaGFsZiBGID0gdURzdFRGX1MwWzZdOwoJaGFsZiBzID0gc2lnbih4KTsKCXggPSBhYnMoeCk7Cgl4ID0gKHggPCBEKSA/IChDICogeCkgKyBGIDogcG93KEEgKiB4ICsgQiwgRykgKyBFOwoJcmV0dXJuIHMgKiB4Owp9CmhhbGY0IGdhbXV0X3hmb3JtX1MwKGhhbGY0IGNvbG9yKSAKewoJY29sb3IucmdiID0gKHVDb2xvclhmb3JtX1MwICogY29sb3IucmdiKTsKCXJldHVybiBjb2xvcjsKfQpoYWxmNCBjb2xvcl94Zm9ybV9TMChmbG9hdDQgY29sb3IpIAp7Cgljb2xvci5yID0gc3JjX3RmX1MwKGhhbGYoY29sb3IucikpOwoJY29sb3IuZyA9IHNyY190Zl9TMChoYWxmKGNvbG9yLmcpKTsKCWNvbG9yLmIgPSBzcmNfdGZfUzAoaGFsZihjb2xvci5iKSk7Cgljb2xvciA9IGdhbXV0X3hmb3JtX1MwKGhhbGY0KGNvbG9yKSk7Cgljb2xvci5yID0gZHN0X3RmX1MwKGhhbGYoY29sb3IucikpOwoJY29sb3IuZyA9IGRzdF90Zl9TMChoYWxmKGNvbG9yLmcpKTsKCWNvbG9yLmIgPSBkc3RfdGZfUzAoaGFsZihjb2xvci5iKSk7CglyZXR1cm4gaGFsZjQoY29sb3IpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKGNvbG9yX3hmb3JtX1MwKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpKSAqIGhhbGY0KDEpKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGACQAGAAAAAQAAAAAAAQQGAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","B2ABSAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CAAAAExTS1N4AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZiBpbkNvdmVyYWdlOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gdUNvbG9yX1MwOwoJY29sb3IgPSBjb2xvciAqIGluQ292ZXJhZ2U7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8zX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzFfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAeAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CAAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAACzAgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAAAAIAAAABLAIABAAAAABAEGABBAMAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADnAgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUKCXJldHVybiBibGVuZF9tb2R1bGF0ZShNYXRyaXhFZmZlY3RfUzFfYzAoX3NyYyksIF9zcmMpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQmxlbmRfUzEob3V0cHV0Q29sb3JfUzAsIGhhbGY0KDEpKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAGIBIAAABAAAAANAEAAAAAAAAAAAAAABAAOAAAABAAAAAAABBAMAAAAA":"CAAAAExTS1N0AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMl9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAIsCAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwKS5ycnJyOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CAAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAkAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAHSADQAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAAWAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACRAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAEDAACATAAABAGYAAAICSBYQCA4AAAAAAEAZCBRE4GNEACAAAOAAAAAAAAAAABAAOAAAABAAAAAAABBAMAA":"CAAAAExTS1OxCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmFyY2Nvb3JkX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0IGFhX2Jsb2F0X211bHRpcGxpZXIgPSAxOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgaXNfbGluZWFyX2NvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnc7CglmbG9hdDIgcGl4ZWxsZW5ndGggPSBpbnZlcnNlc3FydChmbG9hdDIoZG90KHNrZXcueHosIHNrZXcueHopLCBkb3Qoc2tldy55dywgc2tldy55dykpKTsKCWZsb2F0NCBub3JtYWxpemVkX2F4aXNfZGlycyA9IHNrZXcgKiBwaXhlbGxlbmd0aC54eXh5OwoJZmxvYXQyIGF4aXN3aWR0aHMgPSAoYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnh5KSArIGFicyhub3JtYWxpemVkX2F4aXNfZGlycy56dykpOwoJZmxvYXQyIGFhX2Jsb2F0cmFkaXVzID0gYXhpc3dpZHRocyAqIHBpeGVsbGVuZ3RoICogLjU7CglmbG9hdDQgcmFkaWlfYW5kX25laWdoYm9ycyA9IHJhZGlpX3NlbGVjdG9yKiBmbG9hdDR4NChyYWRpaV94LCByYWRpaV95LCByYWRpaV94Lnl4d3osIHJhZGlpX3kud3p5eCk7CglmbG9hdDIgcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnh5OwoJZmxvYXQyIG5laWdoYm9yX3JhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy56dzsKCWZsb2F0IGNvdmVyYWdlX211bHRpcGxpZXIgPSAxOwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlX211bHRpcGxpZXIgPSAxIC8gKG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKSk7CgkJcmFkaWkgPSBmbG9hdDIoMCk7Cgl9CglmbG9hdCBjb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS56OwoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjUpKSkgCgl7CgkJcmFkaWkgPSBmbG9hdDIoMCk7CgkJYWFfYmxvYXRfZGlyZWN0aW9uID0gc2lnbihjb3JuZXIpOwoJCWlmIChjb3ZlcmFnZSA+IC41KSAKCQl7CgkJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IC1hYV9ibG9hdF9kaXJlY3Rpb247CgkJfQoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24gKiBhYV9ibG9hdHJhZGl1cyAqIGFhX2Jsb2F0X211bHRpcGxpZXI7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJaWYgKGNvdmVyYWdlID4gLjUpIAoJewoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueCAhPSAwICYmIHZlcnRleHBvcy54ICogY29ybmVyLnggPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLngpOwoJCQl2ZXJ0ZXhwb3MueCA9IDA7CgkJCXZlcnRleHBvcy55ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci55KSAqIHBpeGVsbGVuZ3RoLnkvcGl4ZWxsZW5ndGgueDsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLngpIC8gKGFicyhjb3JuZXIueCkgKyBiYWNrc2V0KSArIC41OwoJCX0KCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnkgIT0gMCAmJiB2ZXJ0ZXhwb3MueSAqIGNvcm5lci55IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy55KTsKCQkJdmVydGV4cG9zLnkgPSAwOwoJCQl2ZXJ0ZXhwb3MueCArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueCkgKiBwaXhlbGxlbmd0aC54L3BpeGVsbGVuZ3RoLnk7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci55KSAvIChhYnMoY29ybmVyLnkpICsgYmFja3NldCkgKyAuNTsKCQl9Cgl9CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoYXJjY29vcmQueCsxLCBhcmNjb29yZC55KTsKCX0KCXNrX1Bvc2l0aW9uID0gZGV2Y29vcmQueHkwMTsKfQoAAAABAAAABwUAAGNvbnN0IGludCBrRmlsbEFBX1MxID0gMTsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdWNpcmNsZV9TMTsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2YXJjY29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmNsZV9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgZDsKCWlmIChpbnQoMykgPT0ga0ludmVyc2VGaWxsQldfUzEgfHwgaW50KDMpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQlkID0gaGFsZigobGVuZ3RoKCh1Y2lyY2xlX1MxLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfUzEudykgLSAxLjApICogdWNpcmNsZV9TMS56KTsKCX0KCWVsc2UgCgl7CgkJZCA9IGhhbGYoKDEuMCAtIGxlbmd0aCgodWNpcmNsZV9TMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxLncpKSAqIHVjaXJjbGVfUzEueik7Cgl9CglpZiAoaW50KDMpID09IGtGaWxsQUFfUzEgfHwgaW50KDMpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQlyZXR1cm4gaGFsZjQoX2lucHV0ICogc2F0dXJhdGUoZCkpOwoJfQoJZWxzZSAKCXsKCQlyZXR1cm4gaGFsZjQoZCA+IDAuNSA/IF9pbnB1dCA6IGhhbGY0KDAuMCkpOwoJfQp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjbGVfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGAARAGQWMHGBRIAAAAABQAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABhBAAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjbGVfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGQ7CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxLncpIC0gMS4wKSAqIHVjaXJjbGVfUzEueik7Cgl9CgllbHNlIAoJewoJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJaWYgKGludCgxKSA9PSBrRmlsbEFBX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIHNhdHVyYXRlKGQpKTsKCX0KCWVsc2UgCgl7CgkJcmV0dXJuIGhhbGY0KGQgPiAwLjUgPyBfaW5wdXQgOiBoYWxmNCgwLjApKTsKCX0KfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJZmxvYXQyIHRleENvb3JkOwoJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TMDsKCW91dHB1dENvbG9yX1MwID0gKChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHRleENvb3JkKSAqIGhhbGY0KDEpKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY2xlX1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAABAEAAAABJYQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAADEGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbNF07CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbNF07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAueSwgdWNsYW1wX1MxX2MwX2MwX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCwgZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMCkgKiBfY29vcmRzLnh5MSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfM19jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzVfY29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKCWZvciAoaW50IF82X2kgPSAwOyAoXzZfaSA8IDEzKTsgXzZfaSsrKSAoXzNfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChfNV9jb29yZCArIGZsb2F0MigodV8yX09mZnNldHNfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0gKiB1XzBfSW5jcmVtZW50X1MxX2MwKSkpKSAqIHVfMV9LZXJuZWxfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0pKTsKCXJldHVybiBfM19jb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAEDAACATAAABAGYAAAICSBYQCA4AAAAAAEADZAAAAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CAAAAExTS1OxCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmFyY2Nvb3JkX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0IGFhX2Jsb2F0X211bHRpcGxpZXIgPSAxOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgaXNfbGluZWFyX2NvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnc7CglmbG9hdDIgcGl4ZWxsZW5ndGggPSBpbnZlcnNlc3FydChmbG9hdDIoZG90KHNrZXcueHosIHNrZXcueHopLCBkb3Qoc2tldy55dywgc2tldy55dykpKTsKCWZsb2F0NCBub3JtYWxpemVkX2F4aXNfZGlycyA9IHNrZXcgKiBwaXhlbGxlbmd0aC54eXh5OwoJZmxvYXQyIGF4aXN3aWR0aHMgPSAoYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnh5KSArIGFicyhub3JtYWxpemVkX2F4aXNfZGlycy56dykpOwoJZmxvYXQyIGFhX2Jsb2F0cmFkaXVzID0gYXhpc3dpZHRocyAqIHBpeGVsbGVuZ3RoICogLjU7CglmbG9hdDQgcmFkaWlfYW5kX25laWdoYm9ycyA9IHJhZGlpX3NlbGVjdG9yKiBmbG9hdDR4NChyYWRpaV94LCByYWRpaV95LCByYWRpaV94Lnl4d3osIHJhZGlpX3kud3p5eCk7CglmbG9hdDIgcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnh5OwoJZmxvYXQyIG5laWdoYm9yX3JhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy56dzsKCWZsb2F0IGNvdmVyYWdlX211bHRpcGxpZXIgPSAxOwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlX211bHRpcGxpZXIgPSAxIC8gKG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKSk7CgkJcmFkaWkgPSBmbG9hdDIoMCk7Cgl9CglmbG9hdCBjb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS56OwoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjUpKSkgCgl7CgkJcmFkaWkgPSBmbG9hdDIoMCk7CgkJYWFfYmxvYXRfZGlyZWN0aW9uID0gc2lnbihjb3JuZXIpOwoJCWlmIChjb3ZlcmFnZSA+IC41KSAKCQl7CgkJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IC1hYV9ibG9hdF9kaXJlY3Rpb247CgkJfQoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24gKiBhYV9ibG9hdHJhZGl1cyAqIGFhX2Jsb2F0X211bHRpcGxpZXI7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJaWYgKGNvdmVyYWdlID4gLjUpIAoJewoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueCAhPSAwICYmIHZlcnRleHBvcy54ICogY29ybmVyLnggPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLngpOwoJCQl2ZXJ0ZXhwb3MueCA9IDA7CgkJCXZlcnRleHBvcy55ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci55KSAqIHBpeGVsbGVuZ3RoLnkvcGl4ZWxsZW5ndGgueDsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLngpIC8gKGFicyhjb3JuZXIueCkgKyBiYWNrc2V0KSArIC41OwoJCX0KCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnkgIT0gMCAmJiB2ZXJ0ZXhwb3MueSAqIGNvcm5lci55IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy55KTsKCQkJdmVydGV4cG9zLnkgPSAwOwoJCQl2ZXJ0ZXhwb3MueCArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueCkgKiBwaXhlbGxlbmd0aC54L3BpeGVsbGVuZ3RoLnk7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci55KSAvIChhYnMoY29ybmVyLnkpICsgYmFja3NldCkgKyAuNTsKCQl9Cgl9CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoYXJjY29vcmQueCsxLCBhcmNjb29yZC55KTsKCX0KCXNrX1Bvc2l0aW9uID0gZGV2Y29vcmQueHkwMTsKfQoAAAABAAAAPwQAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5ID0gbWF4KHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHksIDAuMCk7CgloYWxmIHJpZ2h0QWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVpbm5lclJlY3RfUzEuUiAtIHNrX0ZyYWdDb29yZC54KSk7CgloYWxmIGJvdHRvbUFscGhhID0gaGFsZihzYXR1cmF0ZSh1aW5uZXJSZWN0X1MxLkIgLSBza19GcmFnQ29vcmQueSkpOwoJaGFsZiBhbHBoYSA9IGJvdHRvbUFscGhhICogcmlnaHRBbHBoYSAqIGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","AYAA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1OCAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMl9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAAKQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAAAAEAAAABJYQAAAAAAAAIAAAAAWCBAAAABAAAAANAECAZAAAAAAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAPIEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzA7CnVuaWZvcm0gaGFsZjIgdV8wX0luY3JlbWVudF9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1XzFfS2VybmVsX1MxX2MwWzRdOwp1bmlmb3JtIGhhbGY0IHVfMl9PZmZzZXRzX1MxX2MwWzRdOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIF9jb29yZHMpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF8zX2NvbG9yID0gaGFsZjQoMC4wKTsKCWZsb2F0MiBfNV9jb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgXzZfaSA9IDA7IChfNl9pIDwgMTMpOyBfNl9pKyspIChfM19jb2xvciArPSAoTWF0cml4RWZmZWN0X1MxX2MwX2MwKF9pbnB1dCwgKF81X2Nvb3JkICsgZmxvYXQyKCh1XzJfT2Zmc2V0c19TMV9jMFsoXzZfaSAvIDQpXVsoXzZfaSAmIDMpXSAqIHVfMF9JbmNyZW1lbnRfUzFfYzApKSkpICogdV8xX0tlcm5lbF9TMV9jMFsoXzZfaSAvIDQpXVsoXzZfaSAmIDMpXSkpOwoJcmV0dXJuIF8zX2NvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","BYIBQAAABQAAIAABBYAAAEIXBAAP777777777777AAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CAAAAExTS1M+AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzNfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAHgEAAGluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAIAAEAAAABJYQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAADEGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbNF07CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbNF07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueCwgdWNsYW1wX1MxX2MwX2MwX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCwgZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMCkgKiBfY29vcmRzLnh5MSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfM19jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzVfY29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKCWZvciAoaW50IF82X2kgPSAwOyAoXzZfaSA8IDEzKTsgXzZfaSsrKSAoXzNfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChfNV9jb29yZCArIGZsb2F0MigodV8yX09mZnNldHNfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0gKiB1XzBfSW5jcmVtZW50X1MxX2MwKSkpKSAqIHVfMV9LZXJuZWxfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0pKTsKCXJldHVybiBfM19jb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAACTAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAADAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAYAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAIAAQAAAAAQGIA":"CAAAAExTS1PlAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc181X1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMSkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAAAAMAcAAHVuaWZvcm0gaGFsZjQgdXN0YXJ0X1MxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVlbmRfUzFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1MwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzFfY29vcmRzID0gX2Nvb3JkczsKCXJldHVybiBoYWxmNChtaXgodXN0YXJ0X1MxX2MwX2MwLCB1ZW5kX1MxX2MwX2MwLCBoYWxmKF90bXBfMV9jb29yZHMueCkpKTsKfQpoYWxmNCBMaW5lYXJMYXlvdXRfUzFfYzBfYzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8yX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8zX2Nvb3JkcyA9IHZUcmFuc2Zvcm1lZENvb3Jkc181X1MwOwoJcmV0dXJuIGhhbGY0KGhhbGY0KGhhbGYoX3RtcF8zX2Nvb3Jkcy54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDYsIDEuMCwgMC4wLCAwLjApKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckxheW91dF9TMV9jMF9jMV9jMChfaW5wdXQpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzRfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoX3RtcF80X2luQ29sb3IpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIWJvb2woaW50KDEpKSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKF90bXBfNF9pbkNvbG9yLCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglpZiAoYm9vbChpbnQoMSkpKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShoYWxmNCBfaW5wdXQpIAp7CglfaW5wdXQgPSBDbGFtcGVkR3JhZGllbnRfUzFfYzAoX2lucHV0KTsKCWhhbGY0IF90bXBfNV9pbkNvbG9yID0gX2lucHV0OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IGNvdmVyYWdlID0gdmNvdmVyYWdlX1MwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSAoaGFsZjQoMS4wKSAtIG91dHB1dF9TMSkgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAEAQAAAAGQCBAMQACAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAGUDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMC54eSwgdWNsYW1wX1MxX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAyQEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","HVJAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAABAAAAAABBAMABAAOAAAABAAAAAAABBAMAAA":"CAAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAADoAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfUzApKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQACAAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAADkAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5ID0gbWF4KHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHksIDAuMCk7CgloYWxmIHJpZ2h0QWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVpbm5lclJlY3RfUzEuUiAtIHNrX0ZyYWdDb29yZC54KSk7CgloYWxmIGJvdHRvbUFscGhhID0gaGFsZihzYXR1cmF0ZSh1aW5uZXJSZWN0X1MxLkIgLSBza19GcmFnQ29vcmQueSkpOwoJaGFsZiBhbHBoYSA9IGJvdHRvbUFscGhhICogcmlnaHRBbHBoYSAqIGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAEDAACATAAABAGYAAAICSBYQCA4AAAAAAEAZIA62YSBDACAAAGAAAAAAAAAAABAAOAAAABAAAAAAABBAMAA":"CAAAAExTS1OxCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmFyY2Nvb3JkX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCWZsb2F0IGFhX2Jsb2F0X211bHRpcGxpZXIgPSAxOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgaXNfbGluZWFyX2NvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnc7CglmbG9hdDIgcGl4ZWxsZW5ndGggPSBpbnZlcnNlc3FydChmbG9hdDIoZG90KHNrZXcueHosIHNrZXcueHopLCBkb3Qoc2tldy55dywgc2tldy55dykpKTsKCWZsb2F0NCBub3JtYWxpemVkX2F4aXNfZGlycyA9IHNrZXcgKiBwaXhlbGxlbmd0aC54eXh5OwoJZmxvYXQyIGF4aXN3aWR0aHMgPSAoYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnh5KSArIGFicyhub3JtYWxpemVkX2F4aXNfZGlycy56dykpOwoJZmxvYXQyIGFhX2Jsb2F0cmFkaXVzID0gYXhpc3dpZHRocyAqIHBpeGVsbGVuZ3RoICogLjU7CglmbG9hdDQgcmFkaWlfYW5kX25laWdoYm9ycyA9IHJhZGlpX3NlbGVjdG9yKiBmbG9hdDR4NChyYWRpaV94LCByYWRpaV95LCByYWRpaV94Lnl4d3osIHJhZGlpX3kud3p5eCk7CglmbG9hdDIgcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnh5OwoJZmxvYXQyIG5laWdoYm9yX3JhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy56dzsKCWZsb2F0IGNvdmVyYWdlX211bHRpcGxpZXIgPSAxOwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlX211bHRpcGxpZXIgPSAxIC8gKG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKSk7CgkJcmFkaWkgPSBmbG9hdDIoMCk7Cgl9CglmbG9hdCBjb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS56OwoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjUpKSkgCgl7CgkJcmFkaWkgPSBmbG9hdDIoMCk7CgkJYWFfYmxvYXRfZGlyZWN0aW9uID0gc2lnbihjb3JuZXIpOwoJCWlmIChjb3ZlcmFnZSA+IC41KSAKCQl7CgkJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IC1hYV9ibG9hdF9kaXJlY3Rpb247CgkJfQoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoICogMS41LCAyIC0gcGl4ZWxsZW5ndGggKiAxLjUpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24gKiBhYV9ibG9hdHJhZGl1cyAqIGFhX2Jsb2F0X211bHRpcGxpZXI7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJaWYgKGNvdmVyYWdlID4gLjUpIAoJewoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueCAhPSAwICYmIHZlcnRleHBvcy54ICogY29ybmVyLnggPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLngpOwoJCQl2ZXJ0ZXhwb3MueCA9IDA7CgkJCXZlcnRleHBvcy55ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci55KSAqIHBpeGVsbGVuZ3RoLnkvcGl4ZWxsZW5ndGgueDsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLngpIC8gKGFicyhjb3JuZXIueCkgKyBiYWNrc2V0KSArIC41OwoJCX0KCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnkgIT0gMCAmJiB2ZXJ0ZXhwb3MueSAqIGNvcm5lci55IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy55KTsKCQkJdmVydGV4cG9zLnkgPSAwOwoJCQl2ZXJ0ZXhwb3MueCArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueCkgKiBwaXhlbGxlbmd0aC54L3BpeGVsbGVuZ3RoLnk7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci55KSAvIChhYnMoY29ybmVyLnkpICsgYmFja3NldCkgKyAuNTsKCQl9Cgl9CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfUzAueHkgPSBmbG9hdDIoYXJjY29vcmQueCsxLCBhcmNjb29yZC55KTsKCX0KCXNrX1Bvc2l0aW9uID0gZGV2Y29vcmQueHkwMTsKfQoAAAABAAAAdwUAAGNvbnN0IGludCBrRmlsbEJXX1MxID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIBAIAAAABLCIIBAAAAABAEGABBAMAACAIAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAD6AwAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfUzFfYzBfYzAueHksIHVjbGFtcF9TMV9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CgkvLyBCbGVuZCBtb2RlOiBNb2R1bGF0ZQoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TMV9jMChfc3JjKSwgX3NyYyk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb2xvcl9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA=="}} \ No newline at end of file diff --git a/shaders_3.0.1.sksl.json b/shaders_3.0.1.sksl.json new file mode 100644 index 000000000..542a6a498 --- /dev/null +++ b/shaders_3.0.1.sksl.json @@ -0,0 +1 @@ +{"platform":"android","name":"SM G970N","engineRevision":"caaafc5604ee9172293eb84a381be6aadd660317","data":{"HVIACAAAABQAAGAAAQ4AAAAAGQQAARC4GAAAIOCAAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAADAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAYAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAIAAQAAAAAQGIA":"CAAAAExTS1PlAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdCBjb3ZlcmFnZTsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdCB2Y292ZXJhZ2VfUzA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc181X1MwID0gZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMSkgKiBsb2NhbENvb3JkLnh5MTsKCX0KfQoAAAAAAAAAMAcAAHVuaWZvcm0gaGFsZjQgdXN0YXJ0X1MxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVlbmRfUzFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQgdmNvdmVyYWdlX1MwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzFfY29vcmRzID0gX2Nvb3JkczsKCXJldHVybiBoYWxmNChtaXgodXN0YXJ0X1MxX2MwX2MwLCB1ZW5kX1MxX2MwX2MwLCBoYWxmKF90bXBfMV9jb29yZHMueCkpKTsKfQpoYWxmNCBMaW5lYXJMYXlvdXRfUzFfYzBfYzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8yX2luQ29sb3IgPSBfaW5wdXQ7CglmbG9hdDIgX3RtcF8zX2Nvb3JkcyA9IHZUcmFuc2Zvcm1lZENvb3Jkc181X1MwOwoJcmV0dXJuIGhhbGY0KGhhbGY0KGhhbGYoX3RtcF8zX2Nvb3Jkcy54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDYsIDEuMCwgMC4wLCAwLjApKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckxheW91dF9TMV9jMF9jMV9jMChfaW5wdXQpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzRfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfUzFfYzBfYzEoX3RtcF80X2luQ29sb3IpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIWJvb2woaW50KDEpKSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1MxX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsQ29sb3JpemVyX1MxX2MwX2MwKF90bXBfNF9pbkNvbG9yLCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglpZiAoYm9vbChpbnQoMSkpKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBoYWxmNChvdXRDb2xvcik7Cn0KaGFsZjQgRGlzYWJsZUNvdmVyYWdlQXNBbHBoYV9TMShoYWxmNCBfaW5wdXQpIAp7CglfaW5wdXQgPSBDbGFtcGVkR3JhZGllbnRfUzFfYzAoX2lucHV0KTsKCWhhbGY0IF90bXBfNV9pbkNvbG9yID0gX2lucHV0OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWZsb2F0IGNvdmVyYWdlID0gdmNvdmVyYWdlX1MwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSAoaGFsZjQoMS4wKSAtIG91dHB1dF9TMSkgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","BYIBQAAABQAAIAABBYAAAEIXBAAP777777777777AAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CAAAAExTS1M+AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzNfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAHgEAAGluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZjb2xvcl9TMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IAAQAAAAAAAAA=","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAACTAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","GEMAAAYAAEHAAAARC4EAAAQWBQAAAAAAAAAQAAAAIBCAAAGQAEAAAAAQAAAABAEQAEAAAAA":"CAAAAExTS1NUAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBSUmVjdFNoYWRvdwoJdmluU2hhZG93UGFyYW1zX1MwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAjAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGhhbGYzIHZpblNoYWRvd1BhcmFtc19TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CglmbG9hdDIgdXYgPSBmbG9hdDIoc2hhZG93UGFyYW1zLnogKiAoMS4wIC0gZCksIDAuNSk7CgloYWxmIGZhY3RvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdXYpLjAwMHIuYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA4AAABpblNoYWRvd1BhcmFtcwAAAQAAAAAAAAA=","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAYEIBAAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAATQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJCXNrX0ZyYWdDb2xvciA9IHNrX0ZyYWdDb2xvci5hMDAwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAIAAEAAAABJYQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAADEGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbNF07CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbNF07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueCwgdWNsYW1wX1MxX2MwX2MwX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCwgZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMCkgKiBfY29vcmRzLnh5MSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfM19jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzVfY29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKCWZvciAoaW50IF82X2kgPSAwOyAoXzZfaSA8IDEzKTsgXzZfaSsrKSAoXzNfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChfNV9jb29yZCArIGZsb2F0MigodV8yX09mZnNldHNfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0gKiB1XzBfSW5jcmVtZW50X1MxX2MwKSkpKSAqIHVfMV9LZXJuZWxfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0pKTsKCXJldHVybiBfM19jb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAACEA2X4PLOGEAAAAAAAAACAAAAAVQQAAQAAAAAQCDIBCAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAAAAAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoBAAAACwQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBDaXJjbGVCbHVyX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TMS54eSkpICogZmxvYXQodWNpcmNsZURhdGFfUzEudykpOwoJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfUzEueikgKiB1Y2lyY2xlRGF0YV9TMS53OwoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIE1hdHJpeEVmZmVjdF9TMV9jMChfdG1wXzBfaW5Db2xvciwgZmxvYXQyKGhhbGYyKGRpc3QsIDAuNSkpKS53KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZUJsdXJfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","B2AAQAAABQAAIAABBYAAB7777777777774ABICAAAAAAAAAAAAAABUABAAAAAEAAAAAIBEABAAAAA":"CAAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cgl2aW5Db3ZlcmFnZV9TMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IF90bXBfMV9pblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAZQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1MwOwppbiBoYWxmIHZpbkNvdmVyYWdlX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdUNvbG9yX1MwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfUzA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAyQEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CAAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAACRAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TMDsKZmxhdCBpbiBmbG9hdDQgdmdlb21TdWJzZXRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAABAAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQUAAABjb2xvcgAAAAoAAABnZW9tU3Vic2V0AAABAAAAAAAAAA==","AYAA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1OCAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMl9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAACAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAZIA62YSBDACAAAGAAAAAAAAAAABAAOAAAABAAAAAAABBAMAA":"CAAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAAB3BQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZiBjb3ZlcmFnZTsKCWlmIChpbnQoMSkgPT0ga0ZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZihhbGwoZ3JlYXRlclRoYW4oZmxvYXQ0KHNrX0ZyYWdDb29yZC54eSwgdXJlY3RVbmlmb3JtX1MxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TMS54eSwgc2tfRnJhZ0Nvb3JkLnh5KSkpID8gMSA6IDApOwoJfQoJZWxzZSAKCXsKCQloYWxmNCBkaXN0czQgPSBjbGFtcChoYWxmNCgxLjAsIDEuMCwgLTEuMCwgLTEuMCkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIHVyZWN0VW5pZm9ybV9TMSksIDAuMCwgMS4wKTsKCQloYWxmMiBkaXN0czIgPSAoZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3KSAtIDEuMDsKCQljb3ZlcmFnZSA9IGRpc3RzMi54ICogZGlzdHMyLnk7Cgl9CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJY292ZXJhZ2UgPSAxLjAgLSBjb3ZlcmFnZTsKCX0KCXJldHVybiBoYWxmNChfaW5wdXQgKiBjb3ZlcmFnZSk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAEQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABPAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlckV4dGVybmFsT0VTIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AYQQ5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACtBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","JABAAAAABAAACAABBYAAAKAAAMAAGEAAAABRAEAAAEHCAAAAAAAABCAAAAAABAEQAEAAAAA":"CAAAAExTS1MbFgAAY29uc3QgZmxvYXQgUFJFQ0lTSU9OID0gNC4wMDAwMDA7CmNvbnN0IGZsb2F0IE1BWF9GSVhFRF9SRVNPTFZFX0xFVkVMID0gNS4wMDAwMDA7CmNvbnN0IGZsb2F0IE1BWF9GSVhFRF9TRUdNRU5UUyA9IDMyLjAwMDAwMDsKdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVhZmZpbmVNYXRyaXhfUzA7CnVuaWZvcm0gZmxvYXQyIHV0cmFuc2xhdGVfUzA7CmluIGZsb2F0MiByZXNvbHZlTGV2ZWxfYW5kX2lkeDsKaW4gZmxvYXQ0IHAwMTsKaW4gZmxvYXQ0IHAyMzsKaW4gZmxvYXQyIGZhblBvaW50QXR0cmliOwovLyBSZXR1cm5zIHRoZSBsZW5ndGggc3F1YXJlZCBvZiB0aGUgbGFyZ2VzdCBmb3J3YXJkIGRpZmZlcmVuY2UgZnJvbSBXYW5nJ3MgY3ViaWMgZm9ybXVsYS4KZmxvYXQgd2FuZ3NfZm9ybXVsYV9tYXhfZmRpZmZfcG93MihmbG9hdDIgcDAsIGZsb2F0MiBwMSwgZmxvYXQyIHAyLCBmbG9hdDIgcDMsICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZmxvYXQyeDIgbWF0cml4KSAKewoJZmxvYXQyIGQwID0gbWF0cml4ICogKGZtYShmbG9hdDIoLTIpLCBwMSwgcDIpICsgcDApOwoJZmxvYXQyIGQxID0gbWF0cml4ICogKGZtYShmbG9hdDIoLTIpLCBwMiwgcDMpICsgcDEpOwoJcmV0dXJuIG1heChkb3QoZDAsZDApLCBkb3QoZDEsZDEpKTsKfQpmbG9hdCB3YW5nc19mb3JtdWxhX2N1YmljKGZsb2F0IF9wcmVjaXNpb25fLCBmbG9hdDIgcDAsIGZsb2F0MiBwMSwgZmxvYXQyIHAyLCBmbG9hdDIgcDMsICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZmxvYXQyeDIgbWF0cml4KSAKewoJZmxvYXQgbSA9IHdhbmdzX2Zvcm11bGFfbWF4X2ZkaWZmX3BvdzIocDAsIHAxLCBwMiwgcDMsIG1hdHJpeCk7CglyZXR1cm4gbWF4KGNlaWwoc3FydCgwLjc1MDAwMCAqIF9wcmVjaXNpb25fICogc3FydChtKSkpLCAxLjApOwp9CmZsb2F0IHdhbmdzX2Zvcm11bGFfY3ViaWNfbG9nMihmbG9hdCBfcHJlY2lzaW9uXywgZmxvYXQyIHAwLCBmbG9hdDIgcDEsIGZsb2F0MiBwMiwgZmxvYXQyIHAzLCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZmxvYXQyeDIgbWF0cml4KSAKewoJZmxvYXQgbSA9IHdhbmdzX2Zvcm11bGFfbWF4X2ZkaWZmX3BvdzIocDAsIHAxLCBwMiwgcDMsIG1hdHJpeCk7CglyZXR1cm4gY2VpbChsb2cyKG1heCgwLjU2MjUwMCAqIF9wcmVjaXNpb25fICogX3ByZWNpc2lvbl8gKiBtLCAxLjApKSAqIC4yNSk7Cn0KZmxvYXQgd2FuZ3NfZm9ybXVsYV9jb25pY19wb3cyKGZsb2F0IF9wcmVjaXNpb25fLCBmbG9hdDIgcDAsIGZsb2F0MiBwMSwgZmxvYXQyIHAyLCBmbG9hdCB3KSAKewoJLy8gVHJhbnNsYXRlIHRoZSBib3VuZGluZyBib3ggY2VudGVyIHRvIHRoZSBvcmlnaW4uCglmbG9hdDIgQyA9IChtaW4obWluKHAwLCBwMSksIHAyKSArIG1heChtYXgocDAsIHAxKSwgcDIpKSAqIDAuNTsKCXAwIC09IEM7CglwMSAtPSBDOwoJcDIgLT0gQzsKCS8vIENvbXB1dGUgbWF4IGxlbmd0aC4KCWZsb2F0IG0gPSBzcXJ0KG1heChtYXgoZG90KHAwLHAwKSwgZG90KHAxLHAxKSksIGRvdChwMixwMikpKTsKCS8vIENvbXB1dGUgZm9yd2FyZCBkaWZmZXJlbmNlcy4KCWZsb2F0MiBkcCA9IGZtYShmbG9hdDIoLTIuMCAqIHcpLCBwMSwgcDApICsgcDI7CglmbG9hdCBkdyA9IGFicyhmbWEoLTIuMCwgdywgMi4wKSk7CgkvLyBDb21wdXRlIG51bWVyYXRvciBhbmQgZGVub21pbmF0b3IgZm9yIHBhcmFtZXRyaWMgc3RlcCBzaXplIG9mIGxpbmVhcml6YXRpb24uIEhlcmUsIHRoZQoJLy8gZXBzaWxvbiByZWZlcmVuY2VkIGZyb20gdGhlIGNpdGVkIHBhcGVyIGlzIDEvcHJlY2lzaW9uLgoJZmxvYXQgcnBfbWludXNfMSA9IG1heCgwLjAsIGZtYShtLCBfcHJlY2lzaW9uXywgLTEuMCkpOwoJZmxvYXQgbnVtZXIgPSBsZW5ndGgoZHApICogX3ByZWNpc2lvbl8gKyBycF9taW51c18xICogZHc7CglmbG9hdCBkZW5vbSA9IDQgKiBtaW4odywgMS4wKTsKCXJldHVybiBudW1lci9kZW5vbTsKfQpmbG9hdCB3YW5nc19mb3JtdWxhX2NvbmljKGZsb2F0IF9wcmVjaXNpb25fLCBmbG9hdDIgcDAsIGZsb2F0MiBwMSwgZmxvYXQyIHAyLCBmbG9hdCB3KSAKewoJZmxvYXQgbjIgPSB3YW5nc19mb3JtdWxhX2NvbmljX3BvdzIoX3ByZWNpc2lvbl8sIHAwLCBwMSwgcDIsIHcpOwoJcmV0dXJuIG1heChjZWlsKHNxcnQobjIpKSwgMS4wKTsKfQpmbG9hdCB3YW5nc19mb3JtdWxhX2NvbmljX2xvZzIoZmxvYXQgX3ByZWNpc2lvbl8sIGZsb2F0MiBwMCwgZmxvYXQyIHAxLCBmbG9hdDIgcDIsIGZsb2F0IHcpIAp7CglmbG9hdCBuMiA9IHdhbmdzX2Zvcm11bGFfY29uaWNfcG93MihfcHJlY2lzaW9uXywgcDAsIHAxLCBwMiwgdyk7CglyZXR1cm4gY2VpbChsb2cyKG1heChuMiwgMS4wKSkgKiAuNSk7Cn0KYm9vbCBpc19jb25pY19jdXJ2ZSgpIAp7CglyZXR1cm4gaXNpbmYocDIzLncpOwp9CmJvb2wgaXNfdHJpYW5ndWxhcl9jb25pY19jdXJ2ZSgpIAp7CglyZXR1cm4gaXNpbmYocDIzLnopOwp9CmZsb2F0IGxkZXhwX3BvcnRhYmxlKGZsb2F0IHgsIGZsb2F0IHApIAp7CglyZXR1cm4gbGRleHAoeCwgaW50KHApKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciB0ZXNzZWxsYXRlX01pZGRsZU91dFNoYWRlcgoJZmxvYXQyeDIgQUZGSU5FX01BVFJJWCA9IGZsb2F0MngyKHVhZmZpbmVNYXRyaXhfUzApOwoJZmxvYXQyIFRSQU5TTEFURSA9IHV0cmFuc2xhdGVfUzA7CglmbG9hdCByZXNvbHZlTGV2ZWwgPSByZXNvbHZlTGV2ZWxfYW5kX2lkeC54OwoJZmxvYXQgaWR4SW5SZXNvbHZlTGV2ZWwgPSByZXNvbHZlTGV2ZWxfYW5kX2lkeC55OwoJZmxvYXQyIGxvY2FsY29vcmQ7CgkvLyBBIG5lZ2F0aXZlIHJlc29sdmUgbGV2ZWwgbWVhbnMgdGhpcyBpcyB0aGUgZmFuIHBvaW50LgoJaWYgKHJlc29sdmVMZXZlbCA8IDApIAoJewoJCWxvY2FsY29vcmQgPSBmYW5Qb2ludEF0dHJpYjsKCX0KCWVsc2UgICAgICAgICAgICBpZiAoaXNfdHJpYW5ndWxhcl9jb25pY19jdXJ2ZSgpKSAKCXsKCQkvLyBUaGlzIHBhdGNoIGlzIGFuIGV4YWN0IHRyaWFuZ2xlLgoJCWxvY2FsY29vcmQgPSAocmVzb2x2ZUxldmVsICE9IDApICAgICAgPyBwMDEuencgICAgICAgICAgICAgICAgICAgICAgICAgICA6IChpZHhJblJlc29sdmVMZXZlbCAhPSAwKSA/IHAyMy54eSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDogcDAxLnh5OwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgcDA9cDAxLnh5LCBwMT1wMDEuencsIHAyPXAyMy54eSwgcDM9cDIzLnp3OwoJCWZsb2F0IHcgPSAtMTsKCQkvLyB3IDwgMCB0ZWxscyB1cyB0byB0cmVhdCB0aGUgaW5zdGFuY2UgYXMgYW4gaW50ZWdyYWwgY3ViaWMuCgkJZmxvYXQgbWF4UmVzb2x2ZUxldmVsOwoJCWlmIChpc19jb25pY19jdXJ2ZSgpKSAKCQl7CgkJCS8vIENvbmljcyBhcmUgMyBwb2ludHMsIHdpdGggdGhlIHdlaWdodCBpbiBwMy4KCQkJdyA9IHAzLng7CgkJCW1heFJlc29sdmVMZXZlbCA9IHdhbmdzX2Zvcm11bGFfY29uaWNfbG9nMihQUkVDSVNJT04sIEFGRklORV9NQVRSSVggKiBwMCwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFGRklORV9NQVRSSVggKiBwMSwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFGRklORV9NQVRSSVggKiBwMiwgdyk7CgkJCXAxICo9IHc7CgkJCS8vIFVucHJvamVjdCBwMS4KCQkJcDMgPSBwMjsKCQkJLy8gRHVwbGljYXRlIHRoZSBlbmRwb2ludCBmb3Igc2hhcmVkIGNvZGUgdGhhdCBhbHNvIHJ1bnMgb24gY3ViaWNzLgoJCX0KCQllbHNlIAoJCXsKCQkJLy8gVGhlIHBhdGNoIGlzIGFuIGludGVncmFsIGN1YmljLgoJCQltYXhSZXNvbHZlTGV2ZWwgPSB3YW5nc19mb3JtdWxhX2N1YmljX2xvZzIoUFJFQ0lTSU9OLCBwMCwgcDEsIHAyLCBwMywgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBBRkZJTkVfTUFUUklYKTsKCQl9CgkJaWYgKHJlc29sdmVMZXZlbCA+IG1heFJlc29sdmVMZXZlbCkgCgkJewoJCQkvLyBUaGlzIHZlcnRleCBpcyBhdCBhIGhpZ2hlciByZXNvbHZlIGxldmVsIHRoYW4gd2UgbmVlZC4gRGVtb3RlIHRvIGEgbG93ZXIKCQkJLy8gcmVzb2x2ZUxldmVsLCB3aGljaCB3aWxsIHByb2R1Y2UgYSBkZWdlbmVyYXRlIHRyaWFuZ2xlLgoJCQlpZHhJblJlc29sdmVMZXZlbCA9IGZsb29yKGxkZXhwX3BvcnRhYmxlKGlkeEluUmVzb2x2ZUxldmVsLCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXhSZXNvbHZlTGV2ZWwgLSByZXNvbHZlTGV2ZWwpKTsKCQkJcmVzb2x2ZUxldmVsID0gbWF4UmVzb2x2ZUxldmVsOwoJCX0KCQkvLyBQcm9tb3RlIG91ciBsb2NhdGlvbiB0byBhIGRpc2NyZXRlIHBvc2l0aW9uIGluIHRoZSBtYXhpbXVtIGZpeGVkIHJlc29sdmUgbGV2ZWwuCgkJLy8gVGhpcyBpcyBleHRyYSBwYXJhbm9pYSB0byBlbnN1cmUgd2UgZ2V0IHRoZSBleGFjdCBzYW1lIGZwMzIgY29vcmRpbmF0ZXMgZm9yCgkJLy8gY29sb2NhdGVkIHBvaW50cyBmcm9tIGRpZmZlcmVudCByZXNvbHZlIGxldmVscyAoZS5nLiwgdGhlIHZlcnRpY2VzIFQ9My80IGFuZAoJCS8vIFQ9Ni84IHNob3VsZCBiZSBleGFjdGx5IGNvbG9jYXRlZCkuCgkJZmxvYXQgZml4ZWRWZXJ0ZXhJRCA9IGZsb29yKC41ICsgbGRleHBfcG9ydGFibGUoICAgICAgICAgICAgICAgICAgICAgICAgaWR4SW5SZXNvbHZlTGV2ZWwsIE1BWF9GSVhFRF9SRVNPTFZFX0xFVkVMIC0gcmVzb2x2ZUxldmVsKSk7CgkJaWYgKDAgPCBmaXhlZFZlcnRleElEICYmIGZpeGVkVmVydGV4SUQgPCBNQVhfRklYRURfU0VHTUVOVFMpIAoJCXsKCQkJZmxvYXQgVCA9IGZpeGVkVmVydGV4SUQgKiAoMSAvIE1BWF9GSVhFRF9TRUdNRU5UUyk7CgkJCS8vIEV2YWx1YXRlIGF0IFQuIFVzZSBEZSBDYXN0ZWxqYXUncyBmb3IgaXRzIGFjY3VyYWN5IGFuZCBzdGFiaWxpdHkuCgkJCWZsb2F0MiBhYiA9IG1peChwMCwgcDEsIFQpOwoJCQlmbG9hdDIgYmMgPSBtaXgocDEsIHAyLCBUKTsKCQkJZmxvYXQyIGNkID0gbWl4KHAyLCBwMywgVCk7CgkJCWZsb2F0MiBhYmMgPSBtaXgoYWIsIGJjLCBUKTsKCQkJZmxvYXQyIGJjZCA9IG1peChiYywgY2QsIFQpOwoJCQlmbG9hdDIgYWJjZCA9IG1peChhYmMsIGJjZCwgVCk7CgkJCS8vIEV2YWx1YXRlIHRoZSBjb25pYyB3ZWlnaHQgYXQgVC4KCQkJZmxvYXQgdSA9IG1peCgxLjAsIHcsIFQpOwoJCQlmbG9hdCB2ID0gdyArIDEgLSB1OwoJCQkvLyA9PSBtaXgodywgMSwgVCkKCQkJZmxvYXQgdXYgPSBtaXgodSwgdiwgVCk7CgkJCWxvY2FsY29vcmQgPSAodyA8IDApID8gLypjdWJpYyovIGFiY2QgOiAvKmNvbmljKi8gYWJjL3V2OwoJCX0KCQllbHNlIAoJCXsKCQkJbG9jYWxjb29yZCA9IChmaXhlZFZlcnRleElEID09IDApID8gcDAueHkgOiBwMy54eTsKCQl9Cgl9CglmbG9hdDIgdmVydGV4cG9zID0gQUZGSU5FX01BVFJJWCAqIGxvY2FsY29vcmQgKyBUUkFOU0xBVEU7Cglza19Qb3NpdGlvbiA9IHZlcnRleHBvcy54eTAxOwp9CgAAAAAA4QAAAHVuaWZvcm0gaGFsZjQgdWNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgdGVzc2VsbGF0ZV9NaWRkbGVPdXRTaGFkZXIKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gdWNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogRGlzYWJsZSBDb2xvcgoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAQAAAAUAAAAcmVzb2x2ZUxldmVsX2FuZF9pZHgDAAAAcDAxAAMAAABwMjMADgAAAGZhblBvaW50QXR0cmliAAABAAAAAAAAAA==","HWQACAAAABAAADAAAIOAAAAADIIAAIRODAAP577774DSAIAA737777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CAAAAExTS1ONAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQgdmNvdmVyYWdlX1MwOwpmbGF0IG91dCBmbG9hdDQgdmdlb21TdWJzZXRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfUzAgPSBjb2xvcjsKCXZjb3ZlcmFnZV9TMCA9IGNvdmVyYWdlOwoJdmdlb21TdWJzZXRfUzAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAAAgBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdCB2Y292ZXJhZ2VfUzA7CmZsYXQgaW4gZmxvYXQ0IHZnZW9tU3Vic2V0X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TMDsKCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEsIDEsIC0xLCAtMSkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIGdlb1N1YnNldCksIDAsIDEpOwoJaGFsZjIgZGlzdHMyID0gZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3IC0gMTsKCWhhbGYgc3Vic2V0Q292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJY292ZXJhZ2UgPSBtaW4oY292ZXJhZ2UsIHN1YnNldENvdmVyYWdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoaGFsZihjb3ZlcmFnZSkpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAABAAAAAgAAABwb3NpdGlvbggAAABjb3ZlcmFnZQUAAABjb2xvcgAAAAoAAABnZW9tU3Vic2V0AAABAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEADZABYAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CAAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAADsAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","AYTRVAADQAAAOAEARAFQJAABBADAAAILBYAACCYUQD777777777767YAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1NyAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7CmluIGhhbGYzIGluQ2xpcFBsYW5lOwppbiBoYWxmMyBpbklzZWN0UGxhbmU7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKb3V0IGhhbGYzIHZpbklzZWN0UGxhbmVfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfUzAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5DbGlwUGxhbmVfUzAgPSBpbkNsaXBQbGFuZTsKCXZpbklzZWN0UGxhbmVfUzAgPSBpbklzZWN0UGxhbmU7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1MwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TMC55dzsKCXNrX1Bvc2l0aW9uID0gX3RtcF8wX2luUG9zaXRpb24ueHkwMTsKfQoAAAAAAAD1AwAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfUzA7CmluIGhhbGYzIHZpbkNsaXBQbGFuZV9TMDsKaW4gaGFsZjMgdmluSXNlY3RQbGFuZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGYzIGNsaXBQbGFuZTsKCWNsaXBQbGFuZSA9IHZpbkNsaXBQbGFuZV9TMDsKCWhhbGYzIGlzZWN0UGxhbmU7Cglpc2VjdFBsYW5lID0gdmluSXNlY3RQbGFuZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGYgY2xpcCA9IGhhbGYoc2F0dXJhdGUoY2lyY2xlRWRnZS56ICogZG90KGNpcmNsZUVkZ2UueHksIGNsaXBQbGFuZS54eSkgKyBjbGlwUGxhbmUueikpOwoJY2xpcCAqPSBoYWxmKHNhdHVyYXRlKGNpcmNsZUVkZ2UueiAqIGRvdChjaXJjbGVFZGdlLnh5LCBpc2VjdFBsYW5lLnh5KSArIGlzZWN0UGxhbmUueikpOwoJZWRnZUFscGhhICo9IGNsaXA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAFAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2ULAAAAaW5DbGlwUGxhbmUADAAAAGluSXNlY3RQbGFuZQEAAAAAAAAA","HUQAAAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CAAAAExTS1PUAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKfQoAAAAAKQEAAGZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAAAAEAAAABJYQAAAAAAAAIAAAAAWCBAAAABAAAAANAECAZAAAAAAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAPIEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzA7CnVuaWZvcm0gaGFsZjIgdV8wX0luY3JlbWVudF9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1XzFfS2VybmVsX1MxX2MwWzRdOwp1bmlmb3JtIGhhbGY0IHVfMl9PZmZzZXRzX1MxX2MwWzRdOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIF9jb29yZHMpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzBfYzAoX2lucHV0LCBmbG9hdDN4Mih1bWF0cml4X1MxX2MwX2MwKSAqIF9jb29yZHMueHkxKTsKfQpoYWxmNCBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF8zX2NvbG9yID0gaGFsZjQoMC4wKTsKCWZsb2F0MiBfNV9jb29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZm9yIChpbnQgXzZfaSA9IDA7IChfNl9pIDwgMTMpOyBfNl9pKyspIChfM19jb2xvciArPSAoTWF0cml4RWZmZWN0X1MxX2MwX2MwKF9pbnB1dCwgKF81X2Nvb3JkICsgZmxvYXQyKCh1XzJfT2Zmc2V0c19TMV9jMFsoXzZfaSAvIDQpXVsoXzZfaSAmIDMpXSAqIHVfMF9JbmNyZW1lbnRfUzFfYzApKSkpICogdV8xX0tlcm5lbF9TMV9jMFsoXzZfaSAvIDQpXVsoXzZfaSAmIDMpXSkpOwoJcmV0dXJuIF8zX2NvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gTWF0cml4RWZmZWN0X1MxKG91dHB1dENvbG9yX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfUzEgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","HTQAAGAABBYAAAEIXBAAAGEAMAAAAAAAAAAAAAAAQAHAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1M/AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfUzA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgABAAAAHQMAAGluIGZsb2F0NCB2UXVhZEVkZ2VfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZEVkZ2UKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGYgZWRnZUFscGhhOwoJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZRdWFkRWRnZV9TMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1MwLnosIHZRdWFkRWRnZV9TMC53KSArIDAuNSwgMS4wKSk7Cgl9CgllbHNlIAoJewoJCWhhbGYyIGdGID0gaGFsZjIoaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1MwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TMC54KnZRdWFkRWRnZV9TMC54IC0gdlF1YWRFZGdlX1MwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAKAAAAaW5RdWFkRWRnZQAAAQAAAAAAAAA=","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGACQAGAAAAAQAAAAAAAQQGAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAAAAAC3AQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGABZAA6IAAAAACAAAAAADUAAAAAAAEAAAAAIDEAAAAAAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABGAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfUzA7CglvdXRwdXRDb2xvcl9TMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAFBQATAAAAAAFAAMAAAABAAAAAAABBAMAAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAABYBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgRWxsaXB0aWNhbFJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJZmxvYXQyIFogPSBkeHkgKiB1aW52UmFkaWlYWV9TMS54eTsKCWhhbGYgaW1wbGljaXQgPSBoYWxmKGRvdChaLCBkeHkpIC0gMS4wKTsKCWhhbGYgZ3JhZF9kb3QgPSBoYWxmKDQuMCAqIGRvdChaLCBaKSk7CglncmFkX2RvdCA9IG1heChncmFkX2RvdCwgMS4wZS00KTsKCWhhbGYgYXBwcm94X2Rpc3QgPSBpbXBsaWNpdCAqIGhhbGYoaW52ZXJzZXNxcnQoZ3JhZF9kb3QpKTsKCWhhbGYgYWxwaGEgPSBjbGFtcCgwLjUgKyBhcHByb3hfZGlzdCwgMC4wLCAxLjApOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gRWxsaXB0aWNhbFJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAKAAYAAAACAAAAAAACCAYAAA":"CAAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAkAQAAaW4gaGFsZjQgdmNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAAAQAAAAGQCBAMQACAAAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAIgDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzAueCwgdWNsYW1wX1MxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUJAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAQAAAAAAQQGAARAGQWMHGBRIAAAAABQAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CAAAAExTS1PlAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmxvY2FsQ29vcmRfUzAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBwb3NpdGlvbi54eTAxOwp9CgAAAAEAAABhBAAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjbGVfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGQ7CglpZiAoaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1MxLncpIC0gMS4wKSAqIHVjaXJjbGVfUzEueik7Cgl9CgllbHNlIAoJewoJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJaWYgKGludCgxKSA9PSBrRmlsbEFBX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxBQV9TMSkgCgl7CgkJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIHNhdHVyYXRlKGQpKTsKCX0KCWVsc2UgCgl7CgkJcmV0dXJuIGhhbGY0KGQgPiAwLjUgPyBfaW5wdXQgOiBoYWxmNCgwLjApKTsKCX0KfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJZmxvYXQyIHRleENvb3JkOwoJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TMDsKCW91dHB1dENvbG9yX1MwID0gKChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHRleENvb3JkKSAqIGhhbGY0KDEpKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY2xlX1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAKPABAAAAAAB2AAAAAAACAAAAAEBSAAAAAAA":"CAAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAACzBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBmbG9hdDIgdWludlJhZGlpWFlfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBFbGxpcHRpY2FsUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CglmbG9hdDIgWiA9IGR4eSAqIHVpbnZSYWRpaVhZX1MxLnh5OwoJaGFsZiBpbXBsaWNpdCA9IGhhbGYoZG90KFosIGR4eSkgLSAxLjApOwoJaGFsZiBncmFkX2RvdCA9IGhhbGYoNC4wICogZG90KFosIFopKTsKCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjBlLTQpOwoJaGFsZiBhcHByb3hfZGlzdCA9IGltcGxpY2l0ICogaGFsZihpbnZlcnNlc3FydChncmFkX2RvdCkpOwoJaGFsZiBhbHBoYSA9IGNsYW1wKDAuNSArIGFwcHJveF9kaXN0LCAwLjAsIDEuMCk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEVsbGlwdGljYWxSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAQAAABza2V3GQAAAHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUAAAAFAAAAY29sb3IAAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAIBAIAAAABLCIIBAAAAABAEGABBAMAACAIAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADhAwAAdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfUzFfYzBfYzAueHksIHVjbGFtcF9TMV9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IEJsZW5kX1MxKGhhbGY0IF9zcmMsIGhhbGY0IF9kc3QpIAp7CglyZXR1cm4gYmxlbmRfbW9kdWxhdGUoTWF0cml4RWZmZWN0X1MxX2MwKF9zcmMpLCBfc3JjKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IEJsZW5kX1MxKG91dHB1dENvbG9yX1MwLCBoYWxmNCgxKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIAHSADQAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAAWAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEJpdG1hcFRleHQKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gdGV4Q29sb3I7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoBAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACRAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfUzA7CgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmluQ29sb3JfUzA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","HUQACAAAAAMAADAAAIOAAAH677776IZOCAAP577777777777777777YBAAAAAAAAAAAHEADZAAAAAAIAAAAAAOQAAAAAAAQAAAABAMQAAAAAA":"CAAAAExTS1PPAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAEAAACzAgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGhhbGY0IHZjb2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","CIAAAAAAQAARQAAYQAAAAGFYQAABRAAAAEEAAAAAAARAEAEABYAAAAEAAAAAAAEEBQAAAAA":"CAAAAExTS1NVAwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVhdGxhc19hZGp1c3RfUzA7CmluIGZsb2F0NCBmaWxsQm91bmRzOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQ0IGxvY2F0aW9uczsKb3V0IGZsb2F0MiB2YXRsYXNDb29yZF9TMDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEcmF3QXRsYXNQYXRoU2hhZGVyCglmbG9hdDIgdW5pdENvb3JkID0gZmxvYXQyKHNrX1ZlcnRleElEICYgMSwgc2tfVmVydGV4SUQgPj4gMSk7CglmbG9hdDIgZGV2Q29vcmQgPSBtaXgoZmlsbEJvdW5kcy54eSwgZmlsbEJvdW5kcy56dywgdW5pdENvb3JkKTsKCS8vIEEgbmVnYXRpdmUgeCBjb29yZGluYXRlIGluIHRoZSBhdGxhcyBpbmRpY2F0ZXMgdGhhdCB0aGUgcGF0aCBpcyB0cmFuc3Bvc2VkLgoJLy8gV2UgYWxzbyBhZGRlZCAxIHNpbmNlIHdlIGNhbid0IG5lZ2F0ZSB6ZXJvLgoJZmxvYXQyIGF0bGFzVG9wTGVmdCA9IGZsb2F0MihhYnMobG9jYXRpb25zLngpIC0gMSwgbG9jYXRpb25zLnkpOwoJZmxvYXQyIGRldlRvcExlZnQgPSBsb2NhdGlvbnMuenc7Cglib29sIHRyYW5zcG9zZWQgPSBsb2NhdGlvbnMueCA8IDA7CglmbG9hdDIgYXRsYXNDb29yZCA9IGRldkNvb3JkIC0gZGV2VG9wTGVmdDsKCWlmICh0cmFuc3Bvc2VkKSAKCXsKCQlhdGxhc0Nvb3JkID0gYXRsYXNDb29yZC55eDsKCX0KCWF0bGFzQ29vcmQgKz0gYXRsYXNUb3BMZWZ0OwoJdmF0bGFzQ29vcmRfUzAgPSBhdGxhc0Nvb3JkICogdWF0bGFzX2FkanVzdF9TMDsKCXZjb2xvcl9TMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBkZXZDb29yZC54eTAxOwp9CgAAAAAAAADKAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmluIGZsb2F0MiB2YXRsYXNDb29yZF9TMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEcmF3QXRsYXNQYXRoU2hhZGVyCgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJaGFsZiBhdGxhc0NvdmVyYWdlID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2YXRsYXNDb29yZF9TMCkuMDAwci5hOwoJb3V0cHV0Q292ZXJhZ2VfUzAgKj0gYXRsYXNDb3ZlcmFnZTsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAZmlsbEJvdW5kcwAABQAAAGNvbG9yAAAACQAAAGxvY2F0aW9ucwAAAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAAAAB3QA6AAAEAAAAAAAMAAPEAEAAABAAAAAAB2AAAAAAACAAAAAEBSAAAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAA2BQAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfUzI7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1MyOwppbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglhbHBoYSA9IDEuMCAtIGFscGhhOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CmhhbGY0IENpcmN1bGFyUlJlY3RfUzIoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1MyLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1MyLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzIueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCWhhbGY0IG91dHB1dF9TMjsKCW91dHB1dF9TMiA9IENpcmN1bGFyUlJlY3RfUzIob3V0cHV0X1MxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMjsKCX0KfQoAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAAA5AAAAAAABAAAAACAZAAAAA":"CAAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgAAAABdAgAAZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2YXJjY29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1MwLngsIHk9dmFyY2Nvb3JkX1MwLnk7CgloYWxmIGNvdmVyYWdlOwoJaWYgKDAgPT0geF9wbHVzXzEpIAoJewoJCWNvdmVyYWdlID0gaGFsZih5KTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCWZuID0gZm1hKHkseSwgZm4pOwoJCWZsb2F0IGZud2lkdGggPSBmd2lkdGgoZm4pOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","HVJAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAABAAAAAABBAMABAAOAAAABAAAAAAABBAMAAA":"CAAAAExTS1MjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfUzA7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cn0KAAAAAADoAQAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzA7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSAoKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfUzApKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRDb3ZlcmFnZV9TMDsKCX0KfQoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAADUAANAAAAAAAAAIAAAABLAIABAAAAABAEGABBAMAAAAAAAAAAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1M8AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMDsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfM19TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzNfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxX2MwKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAADOAgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1MxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzEsIHZUcmFuc2Zvcm1lZENvb3Jkc18zX1MwKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TMShoYWxmNCBfc3JjLCBoYWxmNCBfZHN0KSAKewoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TMV9jMChfc3JjKSwgX3NyYyk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBCbGVuZF9TMShvdXRwdXRDb2xvcl9TMCwgaGFsZjQoMSkpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABYQACAAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAADkAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5ID0gbWF4KHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHksIDAuMCk7CgloYWxmIHJpZ2h0QWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVpbm5lclJlY3RfUzEuUiAtIHNrX0ZyYWdDb29yZC54KSk7CgloYWxmIGJvdHRvbUFscGhhID0gaGFsZihzYXR1cmF0ZSh1aW5uZXJSZWN0X1MxLkIgLSBza19GcmFnQ29vcmQueSkpOwoJaGFsZiBhbHBoYSA9IGJvdHRvbUFscGhhICogcmlnaHRBbHBoYSAqIGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gQ2lyY3VsYXJSUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACABZQA6AAAEAAAAAAAIADQAAAAIAAAAAAAIIDA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAACnAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfUzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfUzEuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TMS54IC0gbGVuZ3RoKGR4eSkpKTsKCWFscGhhID0gMS4wIC0gYWxwaGE7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1MwOwoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBDaXJjdWxhclJSZWN0X1MxKG91dHB1dENvdmVyYWdlX1MwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dF9TMTsKCX0KfQoAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","DASAAAAAQAAWAABAYAAQBYH7777Z6QQBAEAAAAAAEAAAAAAAEBSAAAB2AAAAAAACAAAAAEBSAAAAA":"CAAAAExTS1PVAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfUzAgPSB1bm9ybVRleENvb3JkcyAqIHVBdGxhc1NpemVJbnZfUzA7Cgl2VGV4SW5kZXhfUzAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gaW5Qb3NpdGlvbi54eTAxOwp9CgAAAAAAAAD4AQAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfUzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1MwOwppbiBmbG9hdDIgdlRleHR1cmVDb29yZHNfUzA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHVDb2xvcl9TMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCk7Cgl9CglvdXRwdXRDb2xvcl9TMCA9IG91dHB1dENvbG9yX1MwICogdGV4Q29sb3I7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQBAEAQAAAAGQCBAMQACAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAGUDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMC54eSwgdWNsYW1wX1MxX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEADZAAAAAAIAAAAAACQAGAAAAAQAAAAAAAQQG":"CAAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAAA/BAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfUzE7CmZsYXQgaW4gaGFsZjQgdmNvbG9yX1MwOwppbiBmbG9hdDIgdmFyY2Nvb3JkX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkgPSBtYXgodWlubmVyUmVjdF9TMS5MVCAtIHNrX0ZyYWdDb29yZC54eSwgMC4wKTsKCWhhbGYgcmlnaHRBbHBoYSA9IGhhbGYoc2F0dXJhdGUodWlubmVyUmVjdF9TMS5SIC0gc2tfRnJhZ0Nvb3JkLngpKTsKCWhhbGYgYm90dG9tQWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVpbm5lclJlY3RfUzEuQiAtIHNrX0ZyYWdDb29yZC55KSk7CgloYWxmIGFscGhhID0gYm90dG9tQWxwaGEgKiByaWdodEFscGhhICogaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfUzEueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmN1bGFyUlJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAEAAAAc2tldxkAAAB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlAAAABQAAAGNvbG9yAAAAAQAAAAAAAAA=","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAIBAEAAAABJYQAAAAAQCAIAAAAAWCBACAIBAAAAANAECAZAAEAQAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAA4GAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbNF07CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbNF07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TMV9jMF9jMF9jMC54eSwgdWNsYW1wX1MxX2MwX2MwX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChfaW5wdXQsIGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzApICogX2Nvb3Jkcy54eTEpOwp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgXzNfY29sb3IgPSBoYWxmNCgwLjApOwoJZmxvYXQyIF81X2Nvb3JkID0gdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzA7Cglmb3IgKGludCBfNl9pID0gMDsgKF82X2kgPCAxMyk7IF82X2krKykgKF8zX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfUzFfYzBfYzAoX2lucHV0LCAoXzVfY29vcmQgKyBmbG9hdDIoKHVfMl9PZmZzZXRzX1MxX2MwWyhfNl9pIC8gNCldWyhfNl9pICYgMyldICogdV8wX0luY3JlbWVudF9TMV9jMCkpKSkgKiB1XzFfS2VybmVsX1MxX2MwWyhfNl9pIC8gNCldWyhfNl9pICYgMyldKSk7CglyZXR1cm4gXzNfY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBHYXVzc2lhbkNvbnZvbHV0aW9uX1MxX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfUzAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1MwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfUzE7CglvdXRwdXRfUzEgPSBNYXRyaXhFZmZlY3RfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","FAAQMYAAMAAAEADAAABAEYAAAICIAB5AABQAAAQAMAAAEATAAABAIIGAAEDCBYQCA4AAAAAAEAZCBRE4GNEACAAAOAAAAAAAAAAABAAOAAAABAAAAAAABBAMAA":"CAAAAExTS1PUCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCByYWRpaV94OwppbiBmbG9hdDQgcmFkaWlfeTsKaW4gZmxvYXQ0IHNrZXc7CmluIGZsb2F0MiB0cmFuc2xhdGVfYW5kX2xvY2Fscm90YXRlOwppbiBoYWxmNCBjb2xvcjsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1MwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1MwID0gY29sb3I7CglmbG9hdCBhYV9ibG9hdF9tdWx0aXBsaWVyID0gMTsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglmbG9hdCBjb3ZlcmFnZV9tdWx0aXBsaWVyID0gMTsKCWlmIChhbnkoZ3JlYXRlclRoYW4oYWFfYmxvYXRyYWRpdXMsIGZsb2F0MigxKSkpKSAKCXsKCQljb3JuZXIgPSBtYXgoYWJzKGNvcm5lciksIGFhX2Jsb2F0cmFkaXVzKSAqIHNpZ24oY29ybmVyKTsKCQljb3ZlcmFnZV9tdWx0aXBsaWVyID0gMSAvIChtYXgoYWFfYmxvYXRyYWRpdXMueCwgMSkgKiBtYXgoYWFfYmxvYXRyYWRpdXMueSwgMSkpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS41KSkpIAoJewoJCXJhZGlpID0gZmxvYXQyKDApOwoJCWFhX2Jsb2F0X2RpcmVjdGlvbiA9IHNpZ24oY29ybmVyKTsKCQlpZiAoY292ZXJhZ2UgPiAuNSkgCgkJewoJCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSAtYWFfYmxvYXRfZGlyZWN0aW9uOwoJCX0KCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCAqIDEuNSwgMiAtIHBpeGVsbGVuZ3RoICogMS41KTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uICogYWFfYmxvYXRyYWRpdXMgKiBhYV9ibG9hdF9tdWx0aXBsaWVyOwoJZmxvYXQyIHZlcnRleHBvcyA9IGNvcm5lciArIHJhZGl1c19vdXRzZXQgKiByYWRpaSArIGFhX291dHNldDsKCWlmIChjb3ZlcmFnZSA+IC41KSAKCXsKCQlpZiAoYWFfYmxvYXRfZGlyZWN0aW9uLnggIT0gMCAmJiB2ZXJ0ZXhwb3MueCAqIGNvcm5lci54IDwgMCkgCgkJewoJCQlmbG9hdCBiYWNrc2V0ID0gYWJzKHZlcnRleHBvcy54KTsKCQkJdmVydGV4cG9zLnggPSAwOwoJCQl2ZXJ0ZXhwb3MueSArPSBiYWNrc2V0ICogc2lnbihjb3JuZXIueSkgKiBwaXhlbGxlbmd0aC55L3BpeGVsbGVuZ3RoLng7CgkJCWNvdmVyYWdlID0gKGNvdmVyYWdlIC0gLjUpICogYWJzKGNvcm5lci54KSAvIChhYnMoY29ybmVyLngpICsgYmFja3NldCkgKyAuNTsKCQl9CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi55ICE9IDAgJiYgdmVydGV4cG9zLnkgKiBjb3JuZXIueSA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueSk7CgkJCXZlcnRleHBvcy55ID0gMDsKCQkJdmVydGV4cG9zLnggKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLngpICogcGl4ZWxsZW5ndGgueC9waXhlbGxlbmd0aC55OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueSkgLyAoYWJzKGNvcm5lci55KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJfQoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUueHk7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MigwLCBjb3ZlcmFnZSAqIGNvdmVyYWdlX211bHRpcGxpZXIpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdDIgYXJjY29vcmQgPSAxIC0gYWJzKHJhZGl1c19vdXRzZXQpICsgYWFfb3V0c2V0L3JhZGlpICogY29ybmVyOwoJCXZhcmNjb29yZF9TMC54eSA9IGZsb2F0MihhcmNjb29yZC54KzEsIGFyY2Nvb3JkLnkpOwoJfQoJc2tfUG9zaXRpb24gPSBkZXZjb29yZC54eTAxOwp9CgEAAAAHBQAAY29uc3QgaW50IGtGaWxsQUFfUzEgPSAxOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1MxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TMDsKaW4gZmxvYXQyIHZhcmNjb29yZF9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY2xlX1MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZiBkOwoJaWYgKGludCgzKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWQgPSBoYWxmKChsZW5ndGgoKHVjaXJjbGVfUzEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TMS53KSAtIDEuMCkgKiB1Y2lyY2xlX1MxLnopOwoJfQoJZWxzZSAKCXsKCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1MxLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfUzEudykpICogdWNpcmNsZV9TMS56KTsKCX0KCWlmIChpbnQoMykgPT0ga0ZpbGxBQV9TMSB8fCBpbnQoMykgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCXJldHVybiBoYWxmNChfaW5wdXQgKiBzYXR1cmF0ZShkKSk7Cgl9CgllbHNlIAoJewoJCXJldHVybiBoYWxmNChkID4gMC41ID8gX2lucHV0IDogaGFsZjQoMC4wKSk7Cgl9Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIEZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2Y29sb3JfUzA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfUzAueCwgeT12YXJjY29vcmRfUzAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IENpcmNsZV9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAQAAABza2V3GQAAAHRyYW5zbGF0ZV9hbmRfbG9jYWxyb3RhdGUAAAAFAAAAY29sb3IAAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQLAAAAAAABAEAAAABJWQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAADEGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbM107CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbM107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAueSwgdWNsYW1wX1MxX2MwX2MwX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCwgZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMCkgKiBfY29vcmRzLnh5MSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfM19jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzVfY29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKCWZvciAoaW50IF82X2kgPSAwOyAoXzZfaSA8IDEyKTsgXzZfaSsrKSAoXzNfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChfNV9jb29yZCArIGZsb2F0MigodV8yX09mZnNldHNfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0gKiB1XzBfSW5jcmVtZW50X1MxX2MwKSkpKSAqIHVfMV9LZXJuZWxfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0pKTsKCXJldHVybiBfM19jb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQAAEAQAAAAGQCBAMQAAAIAAAAAACQAGAAAAAQAAAAAAAQQGAAAAA":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAAIgDAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18yX1MwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzAueSwgdWNsYW1wX1MxX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","DBAAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAIAAAAAAAAAAAAAAAAAAAAOQAAAAAAAQAAAABAMQAAAAA":"CAAAAExTS1NVAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludDIgY29vcmRzID0gaW50MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJaW50IHRleElkeCA9IGNvb3Jkcy54ID4+IDEzOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGNvb3Jkcy54ICYgMHgxRkZGLCBjb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAAAAAAAAFoCAAB1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzFfUzA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfUzA7CmluIGhhbGY0IHZpbkNvbG9yX1MwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7CglpZiAodlRleEluZGV4X1MwID09IDApIAoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MwLCB2VGV4dHVyZUNvb3Jkc19TMCkucnJycjsKCX0KCWVsc2UgCgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzFfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TMCAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQMAAAAAAABAEAAAABJYQAAAAAACAIAAAAAWCBAAAIBAAAAANAECAZAAAAQAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAADEGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbNF07CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbNF07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfUzFfYzBfYzBfYzAueSwgdWNsYW1wX1MxX2MwX2MwX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCwgZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMCkgKiBfY29vcmRzLnh5MSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfM19jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzVfY29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKCWZvciAoaW50IF82X2kgPSAwOyAoXzZfaSA8IDEzKTsgXzZfaSsrKSAoXzNfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChfNV9jb29yZCArIGZsb2F0MigodV8yX09mZnNldHNfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0gKiB1XzBfSW5jcmVtZW50X1MxX2MwKSkpKSAqIHVfMV9LZXJuZWxfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0pKTsKCXJldHVybiBfM19jb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HUIAAAAAAAQAADAAAIOAAAH677777777777QGHAQAD7P7777777777YBAAAAAAAAAAALUAQLAAAAAAIAAEAAAABJWQAAAAAQAAIAAAAAWCBACAABAAAAANAECAZAAEAAAAAAAAFAAMAAAABAAAAAAABBAM":"CAAAAExTS1M2AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IHBvc2l0aW9uLnh5MDE7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzJfUzAgPSBmbG9hdDN4Mih1bWF0cml4X1MxKSAqIGxvY2FsQ29vcmQueHkxOwoJfQp9CgAAAAAAADEGAAB1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfUzFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1MxX2MwOwp1bmlmb3JtIGhhbGY0IHVfMV9LZXJuZWxfUzFfYzBbM107CnVuaWZvcm0gaGFsZjQgdV8yX09mZnNldHNfUzFfYzBbM107CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfUzE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfUzFfYzBfYzBfYzAueCwgdWNsYW1wX1MxX2MwX2MwX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1MxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1MxX2MwX2MwX2MwKF9pbnB1dCwgZmxvYXQzeDIodW1hdHJpeF9TMV9jMF9jMCkgKiBfY29vcmRzLnh5MSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfM19jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzVfY29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMl9TMDsKCWZvciAoaW50IF82X2kgPSAwOyAoXzZfaSA8IDEyKTsgXzZfaSsrKSAoXzNfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TMV9jMF9jMChfaW5wdXQsIChfNV9jb29yZCArIGZsb2F0MigodV8yX09mZnNldHNfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0gKiB1XzBfSW5jcmVtZW50X1MxX2MwKSkpKSAqIHVfMV9LZXJuZWxfUzFfYzBbKF82X2kgLyA0KV1bKF82X2kgJiAzKV0pKTsKCXJldHVybiBfM19jb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fUzFfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IE1hdHJpeEVmZmVjdF9TMShvdXRwdXRDb2xvcl9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1MxICogb3V0cHV0Q292ZXJhZ2VfUzA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAADAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAYAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIA":"CAAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAOMGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgxKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IENsYW1wZWRHcmFkaWVudF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF81X2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","DAQAAAAAAABGAABAYAAQAIHCAIAYAQUBAEAAAAAAEAAAAAAAAAAAAIBSQB5VRECGAEAAAMAAAAAAAAAAACAA4AAAACAAAAAAACCAYAA":"CAAAAExTS1MWAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfUzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiB1c2hvcnQyIGluVGV4dHVyZUNvb3JkczsKb3V0IGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TMDsKZmxhdCBvdXQgZmxvYXQgdlRleEluZGV4X1MwOwpvdXQgaGFsZjQgdmluQ29sb3JfUzA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEJpdG1hcFRleHQKCWludCB0ZXhJZHggPSAwOwoJZmxvYXQyIHVub3JtVGV4Q29vcmRzID0gZmxvYXQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7Cgl2VGV4dHVyZUNvb3Jkc19TMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzU2l6ZUludl9TMDsKCXZUZXhJbmRleF9TMCA9IGZsb2F0KHRleElkeCk7Cgl2aW5Db2xvcl9TMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBpblBvc2l0aW9uLnh5MDE7Cn0KAAABAAAA4wQAAGNvbnN0IGludCBrRmlsbEJXX1MxID0gMDsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEJXX1MxID0gMjsKY29uc3QgaW50IGtJbnZlcnNlRmlsbEFBX1MxID0gMzsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1MxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1MwOwpmbGF0IGluIGZsb2F0IHZUZXhJbmRleF9TMDsKaW4gaGFsZjQgdmluQ29sb3JfUzA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFJlY3RfUzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX3RtcF8wX2luQ29sb3IgPSBfaW5wdXQ7CgloYWxmIGNvdmVyYWdlOwoJaWYgKGludCgxKSA9PSBrRmlsbEJXX1MxIHx8IGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fUzEuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1MxLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7Cgl9CgllbHNlIAoJewoJCWhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1MxKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCWlmIChpbnQoMSkgPT0ga0ludmVyc2VGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEFBX1MxKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIGhhbGY0KF9pbnB1dCAqIGNvdmVyYWdlKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfUzA7CglvdXRwdXRDb2xvcl9TMCA9IHZpbkNvbG9yX1MwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfUzAsIHZUZXh0dXJlQ29vcmRzX1MwKS5ycnJyOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IFJlY3RfUzEob3V0cHV0Q292ZXJhZ2VfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1MwICogb3V0cHV0X1MxOwoJfQp9CgABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","HVIAAAAAABIAAGAAAQ4AAAH477776R24EAAAIOBQAD6P7777777777YDAAAAAAAAAAAFIBTWV34ISAIAAAAEYT7ZOFIQAAAABAAAAABQAAAAAIFGB7HB2BAAAAAFQRH6PYAAAAEAAAAAAAAZGE66LR2FAEAAAIAAAAAMAAAAACAJQPRYO4IAAAAAMAI7T6YBAAAAABAAAAAGIMFGB7HB2BAAAAAAAAAAQAKYCRPE54DQAAAABAAAAAEQEFMEJ7T6AAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIA":"CAAAAExTS1OAAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TMV9jMF9jMTsKaW4gZmxvYXQyIHBvc2l0aW9uOwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfUzAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gcG9zaXRpb24ueHkwMTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfNV9TMCA9IGZsb2F0M3gyKHVtYXRyaXhfUzFfYzBfYzEpICogbG9jYWxDb29yZC54eTE7Cgl9Cn0KAAAAAOMGAAB1bmlmb3JtIGhhbGY0IHVzdGFydF9TMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1ZW5kX1MxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfUzFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TMV9jMDsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfUzA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfNV9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF90bXBfMF9pbkNvbG9yID0gX2lucHV0OwoJZmxvYXQyIF90bXBfMV9jb29yZHMgPSBfY29vcmRzOwoJcmV0dXJuIGhhbGY0KG1peCh1c3RhcnRfUzFfYzBfYzAsIHVlbmRfUzFfYzBfYzAsIGhhbGYoX3RtcF8xX2Nvb3Jkcy54KSkpOwp9CmhhbGY0IExpbmVhckxheW91dF9TMV9jMF9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzJfaW5Db2xvciA9IF9pbnB1dDsKCWZsb2F0MiBfdG1wXzNfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzVfUzA7CglyZXR1cm4gaGFsZjQoaGFsZjQoaGFsZihfdG1wXzNfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCkpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyTGF5b3V0X1MxX2MwX2MxX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50X1MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF90bXBfNF9pbkNvbG9yID0gX2lucHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TMV9jMF9jMShfdG1wXzRfaW5Db2xvcik7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghYm9vbChpbnQoMSkpICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfUzFfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxDb2xvcml6ZXJfUzFfYzBfYzAoX3RtcF80X2luQ29sb3IsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCWlmIChib29sKGludCgwKSkpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIGhhbGY0KG91dENvbG9yKTsKfQpoYWxmNCBEaXNhYmxlQ292ZXJhZ2VBc0FscGhhX1MxKGhhbGY0IF9pbnB1dCkgCnsKCV9pbnB1dCA9IENsYW1wZWRHcmFkaWVudF9TMV9jMChfaW5wdXQpOwoJaGFsZjQgX3RtcF81X2luQ29sb3IgPSBfaW5wdXQ7CglyZXR1cm4gaGFsZjQoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TMDsKCW91dHB1dENvbG9yX1MwID0gdmNvbG9yX1MwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TMTsKCW91dHB1dF9TMSA9IERpc2FibGVDb3ZlcmFnZUFzQWxwaGFfUzEob3V0cHV0Q29sb3JfUzApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TMSAqIG91dHB1dENvdmVyYWdlX1MwOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AYQA5AADQAAAOAEARAFQJAABBADIB7777777777777777777777767YAAAAAAAAAACAMQAHOMFARUBIAADAAAAAAAAAAAAAAHIAAAAAAAIAAAAAQGIAA":"CAAAAExTS1PMAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIGZsb2F0NCBpbkNpcmNsZUVkZ2U7Cm91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TMDsKb3V0IGhhbGY0IHZpbkNvbG9yX1MwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1MwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfUzAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1MwLnl3OwoJc2tfUG9zaXRpb24gPSBfdG1wXzBfaW5Qb3NpdGlvbi54eTAxOwp9CgEAAAAcBQAAY29uc3QgaW50IGtGaWxsQldfUzEgPSAwOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQldfUzEgPSAyOwpjb25zdCBpbnQga0ludmVyc2VGaWxsQUFfUzEgPSAzOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fUzE7CmluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1MwOwppbiBoYWxmNCB2aW5Db2xvcl9TMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgUmVjdF9TMShoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfdG1wXzBfaW5Db2xvciA9IF9pbnB1dDsKCWhhbGYgY292ZXJhZ2U7CglpZiAoaW50KDEpID09IGtGaWxsQldfUzEgfHwgaW50KDEpID09IGtJbnZlcnNlRmlsbEJXX1MxKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fUzEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fUzEpLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJaWYgKGludCgxKSA9PSBrSW52ZXJzZUZpbGxCV19TMSB8fCBpbnQoMSkgPT0ga0ludmVyc2VGaWxsQUFfUzEpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gaGFsZjQoX2lucHV0ICogY292ZXJhZ2UpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TMDsKCWhhbGY0IG91dHB1dENvbG9yX1MwOwoJb3V0cHV0Q29sb3JfUzAgPSB2aW5Db2xvcl9TMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfUzAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1MxOwoJb3V0cHV0X1MxID0gUmVjdF9TMShvdXRwdXRDb3ZlcmFnZV9TMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfUzAgKiBvdXRwdXRfUzE7Cgl9Cn0KAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA="}} \ No newline at end of file diff --git a/test/fake/media_file_service.dart b/test/fake/media_file_service.dart index 71223b179..6ae81b5af 100644 --- a/test/fake/media_file_service.dart +++ b/test/fake/media_file_service.dart @@ -25,6 +25,7 @@ class FakeMediaFileService extends Fake implements MediaFileService { 'path': '${entry.directory}/$newName', 'dateModifiedSecs': FakeMediaStoreService.dateSecs, }, + deleted: false, )); } } diff --git a/test/fake/media_store_service.dart b/test/fake/media_store_service.dart index 813137f95..e6172611e 100644 --- a/test/fake/media_store_service.dart +++ b/test/fake/media_store_service.dart @@ -57,6 +57,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService { 'path': entry.path!.replaceFirst(sourceAlbum, destinationAlbum), 'dateModifiedSecs': FakeMediaStoreService.dateSecs, }, + deleted: false, ); } @@ -73,6 +74,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService { 'path': entry.path!.replaceFirst(oldName, newName), 'dateModifiedSecs': FakeMediaStoreService.dateSecs, }, + deleted: false, ); } } diff --git a/test/model/video/metadata_test.dart b/test/model/video/metadata_test.dart index 0b8474f38..b3b3a3c13 100644 --- a/test/model/video/metadata_test.dart +++ b/test/model/video/metadata_test.dart @@ -10,4 +10,9 @@ void main() { 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); }); + + test('Ambiguous date', () { + expect(VideoMetadataFormatter.isAmbiguousDate('2011-05-08T03:46+09:00'), false); + expect(VideoMetadataFormatter.isAmbiguousDate('05-08-2011'), true); + }); } diff --git a/test/utils/geo_utils_test.dart b/test/utils/geo_utils_test.dart index 5f2cf7ecd..d1ef6c306 100644 --- a/test/utils/geo_utils_test.dart +++ b/test/utils/geo_utils_test.dart @@ -1,6 +1,6 @@ import 'package:aves/l10n/l10n.dart'; import 'package:aves/model/settings/enums/coordinate_format.dart'; -import 'package:aves/utils/geo_utils.dart'; +import 'package:aves_map/aves_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:test/test.dart'; diff --git a/test_driver/driver_screenshots.dart b/test_driver/driver_screenshots.dart index 515018fc6..58b35c7ab 100644 --- a/test_driver/driver_screenshots.dart +++ b/test_driver/driver_screenshots.dart @@ -3,6 +3,7 @@ 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.dart'; +import 'package:aves_map/src/style.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,15 +18,20 @@ Future configureAndLaunch() async { ..hasAcceptedTerms = true ..isInstalledAppAccessAllowed = true ..isErrorReportingAllowed = false - ..keepScreenOn = KeepScreenOn.always - ..homePage = HomePageSetting.collection + ..themeBrightness = AvesThemeBrightness.dark + ..themeColorMode = AvesThemeColorMode.polychrome ..setTileExtent(CountryListPage.routeName, 112) ..setTileLayout(CountryListPage.routeName, TileLayout.grid) + // navigation + ..keepScreenOn = KeepScreenOn.always + ..homePage = HomePageSetting.collection + ..showBottomNavigationBar = true // collection ..collectionSectionFactor = EntryGroupFactor.month ..collectionSortFactor = EntrySortFactor.date ..collectionBrowsingQuickActions = SettingsDefaults.collectionBrowsingQuickActions ..showThumbnailFavourite = false + ..showThumbnailTag = false ..showThumbnailLocation = false ..hiddenFilters = {} // viewer diff --git a/test_driver/driver_screenshots_test.dart b/test_driver/driver_screenshots_test.dart index 8b1e1505f..e9c2ce7a1 100644 --- a/test_driver/driver_screenshots_test.dart +++ b/test_driver/driver_screenshots_test.dart @@ -95,7 +95,7 @@ void configureCollectionVisibility(AppDebugAction action) { test('configure collection visibility', () async { await driver.tapKeyAndWait('appbar-leading-button'); final verticalPageView = find.byValueKey('drawer-scrollview'); - await driver.scroll(verticalPageView, 0, -600, const Duration(milliseconds: 400)); + await driver.scrollY(verticalPageView, -600); await driver.tapKeyAndWait('drawer-debug'); await driver.tapKeyAndWait('appbar-menu-button'); @@ -146,13 +146,13 @@ void info() { test('3. Info (basic), 4. Info (metadata)', () async { final verticalPageView = find.byValueKey('vertical-pageview'); - await driver.scroll(verticalPageView, 0, -600, const Duration(milliseconds: 400)); + await driver.scrollY(verticalPageView, -600); // tiles may take time to load await Future.delayed(const Duration(seconds: 5)); await _takeScreenshot(driver, '3'); - await driver.scroll(verticalPageView, 0, -680, const Duration(milliseconds: 600)); + await driver.scrollY(verticalPageView, -680); await Future.delayed(const Duration(seconds: 1)); final gpsTile = find.descendant( diff --git a/test_driver/driver_shaders.dart b/test_driver/driver_shaders.dart index ba95337da..8d973b7b3 100644 --- a/test_driver/driver_shaders.dart +++ b/test_driver/driver_shaders.dart @@ -4,6 +4,7 @@ import 'package:aves/main_play.dart' as app; 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_map/src/style.dart'; import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test_driver/driver_shaders_test.dart b/test_driver/driver_shaders_test.dart index 69f34364e..ce48c1eed 100644 --- a/test_driver/driver_shaders_test.dart +++ b/test_driver/driver_shaders_test.dart @@ -60,7 +60,7 @@ void agreeToTerms() { // delay to avoid flaky failures when widget binding is not ready from the start await Future.delayed(const Duration(seconds: 3)); - await driver.scroll(find.text('Terms of Service'), 0, -300, const Duration(milliseconds: 500)); + await driver.scrollY(find.text('Terms of Service'), -300); await driver.tap(find.byValueKey('apps-checkbox')); await driver.tap(find.byValueKey('terms-checkbox')); @@ -189,7 +189,7 @@ void showViewer() { void goToNextImage() { test('[viewer] show next image', () async { final horizontalPageView = find.byValueKey('horizontal-pageview'); - await driver.scroll(horizontalPageView, -600, 0, const Duration(milliseconds: 400)); + await driver.scrollX(horizontalPageView, -500); await Future.delayed(const Duration(seconds: 2)); }); } @@ -228,11 +228,11 @@ void showInfoMetadata() { final verticalPageView = find.byValueKey('vertical-pageview'); print('* scroll down to info'); - await driver.scroll(verticalPageView, 0, -600, const Duration(milliseconds: 400)); + await driver.scrollY(verticalPageView, -600); await Future.delayed(const Duration(seconds: 2)); print('* scroll down to metadata details'); - await driver.scroll(verticalPageView, 0, -800, const Duration(milliseconds: 600)); + await driver.scrollY(verticalPageView, -800); await Future.delayed(const Duration(seconds: 1)); print('* toggle GPS metadata'); @@ -246,7 +246,7 @@ void showInfoMetadata() { await driver.waitUntilNoTransientCallbacks(); print('* scroll up to show app bar'); - await driver.scroll(verticalPageView, 0, 100, const Duration(milliseconds: 400)); + await driver.scrollY(verticalPageView, 100); await Future.delayed(const Duration(seconds: 1)); print('* back to image'); @@ -256,7 +256,7 @@ void showInfoMetadata() { void scrollOffImage() { test('[viewer] scroll off', () async { - await driver.scroll(find.byValueKey('image_view'), 0, 800, const Duration(milliseconds: 600)); + await driver.scrollY(find.byValueKey('image_view'), 800); await Future.delayed(const Duration(seconds: 1)); }); } diff --git a/test_driver/utils/driver_extension.dart b/test_driver/utils/driver_extension.dart index 100c64efb..88f4ea6a2 100644 --- a/test_driver/utils/driver_extension.dart +++ b/test_driver/utils/driver_extension.dart @@ -6,7 +6,13 @@ import 'adb_utils.dart'; extension ExtraFlutterDriver on FlutterDriver { static const doubleTapDelay = Duration(milliseconds: 100); // in [kDoubleTapMinTime = 40 ms, kDoubleTapTimeout = 300 ms] - Future doubleTap(SerializableFinder finder, {Duration? timeout}) async { + // scrolling is ineffective when duration is too short for the spatial delta + Future scrollX(SerializableFinder finder, double dx) => scroll(finder, dx, 0, Duration(milliseconds: dx.toInt().abs() * 2)); + + // scrolling is ineffective when duration is too short for the spatial delta + Future scrollY(SerializableFinder finder, double dy) => scroll(finder, 0, dy, Duration(milliseconds: dy.toInt().abs() * 2)); + + Future doubleTap(SerializableFinder finder, {Duration? timeout}) async { await tap(finder, timeout: timeout); await Future.delayed(doubleTapDelay); await tap(finder, timeout: timeout); diff --git a/untranslated.json b/untranslated.json index 1495c4f1e..7735d6704 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,27 +1,13 @@ { "es": [ - "entryActionShowGeoTiffOnMap", - "entryActionConvertMotionPhotoToStillImage", - "entryActionViewMotionPhotoVideo", - "setCoverDialogAuto", - "convertMotionPhotoToStillImageWarningDialogMessage", - "coverDialogTabCover", - "coverDialogTabApp", - "coverDialogTabColor", - "appPickDialogTitle", - "appPickDialogNone" + "settingsShowBottomNavigationBar", + "settingsThumbnailShowTagIcon" ], - "ja": [ - "entryActionShowGeoTiffOnMap", - "entryActionConvertMotionPhotoToStillImage", - "entryActionViewMotionPhotoVideo", - "setCoverDialogAuto", - "convertMotionPhotoToStillImageWarningDialogMessage", - "coverDialogTabCover", - "coverDialogTabApp", - "coverDialogTabColor", - "appPickDialogTitle", - "appPickDialogNone" + "ru": [ + "settingsSearchFieldLabel", + "settingsSearchEmpty", + "settingsShowBottomNavigationBar", + "settingsThumbnailShowTagIcon" ] } diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index e08d6cb67..670d67c49 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,6 +1,6 @@ Thanks for using Aves! -In v1.6.4: -- customize album cover app & color -- explore improved GeoTIFF metadata -- enjoy the app in Italian & Chinese (Simplified) +In v1.6.5: +- bottom navigation bar +- fast scroll with breadcrumbs +- settings search Full changelog available on GitHub \ No newline at end of file