diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a3c0ca863..1a36f459a 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -14,8 +14,8 @@ jobs: steps: - uses: subosito/flutter-action@v1 with: - channel: beta - flutter-version: '2.2.0-10.1.pre' + channel: stable + flutter-version: '2.2.1' - name: Clone the repository. uses: actions/checkout@v2 @@ -31,4 +31,4 @@ jobs: run: flutter analyze - name: Unit tests. - run: flutter test + run: flutter test --no-sound-null-safety diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc38288d9..10d76222d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,8 +16,8 @@ jobs: - uses: subosito/flutter-action@v1 with: - channel: beta - flutter-version: '2.2.0-10.1.pre' + channel: stable + flutter-version: '2.2.1' # Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1): # https://issuetracker.google.com/issues/144111441 @@ -38,7 +38,7 @@ jobs: run: flutter analyze - name: Unit tests. - run: flutter test + run: flutter test --no-sound-null-safety - name: Build signed artifacts. # `KEY_JKS` should contain the result of: @@ -50,8 +50,8 @@ jobs: echo "${{ secrets.KEY_JKS }}" > release.keystore.asc gpg -d --passphrase "${{ secrets.KEY_JKS_PASSPHRASE }}" --batch release.keystore.asc > $AVES_STORE_FILE rm release.keystore.asc - flutter build apk --bundle-sksl-path shaders_2.2.0-10.1.pre.sksl.json - flutter build appbundle --bundle-sksl-path shaders_2.2.0-10.1.pre.sksl.json + flutter build apk --bundle-sksl-path shaders_2.2.1.sksl.json + flutter build appbundle --bundle-sksl-path shaders_2.2.1.sksl.json rm $AVES_STORE_FILE env: AVES_STORE_FILE: ${{ github.workspace }}/key.jks diff --git a/.gitignore b/.gitignore index f3c205341..0fa6b675c 100644 --- a/.gitignore +++ b/.gitignore @@ -40,5 +40,7 @@ app.*.symbols # Obfuscation related app.*.map.json -# Exceptions to above rules. -!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/CHANGELOG.md b/CHANGELOG.md index f8dceea69..9f0f2efe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,26 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [v1.4.2] - 2021-06-10 +### Added +- Collection: snack bar action to show moved/copied/exported entries +- Collection / Albums / Countries / Tags: when switching device orientation, keep items in view +- Collection: when leaving entry from Viewer, make entry visible in collection +- Viewer: fixed layout & minimap for videos with non-square pixels + +### Changed +- upgraded Flutter to stable v2.2.1 +- migrated to unsound null safety +- Collection / Viewer: improved performance, memory usage +- Collection: thumbnail layout change + +### Removed +- no support for Android KitKat (API 19), unsupported by Google Maps package + +### Fixed +- fixed opening files shared via content URI with incorrect MIME type +- refresh collection when entries modified in Viewer no longer match collection filters + ## [v1.4.1] - 2021-04-29 ### Added - Motion photo support diff --git a/README.md b/README.md index 8686402ff..ea63e1349 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt - search and filter by country, place, XMP tag, type (animated, raster, vector…) - favorites - statistics -- support Android API 19 ~ 30 (KitKat ~ R) +- support Android API 20 ~ 30 (Lollipop ~ R) - Android integration (app shortcuts, handle view/pick intents) ## Known Issues diff --git a/analysis_options.yaml b/analysis_options.yaml index 05191c021..537f9f075 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -29,6 +29,6 @@ linter: unnecessary_lambdas: true # misc - prefer_const_constructors: false # too noisy + prefer_const_constructors: true # should specify `const` as Dart does not build constants when using const constructors without it prefer_const_constructors_in_immutables: true prefer_const_declarations: true diff --git a/android/app/build.gradle b/android/app/build.gradle index de11a53bb..16db454f8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -53,8 +53,8 @@ android { defaultConfig { applicationId "deckers.thibault.aves" - minSdkVersion 19 - targetSdkVersion 30 // same as compileSdkVersion + minSdkVersion 20 + targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey']] @@ -100,16 +100,16 @@ flutter { repositories { maven { url 'https://jitpack.io' } - maven { url "https://s3.amazonaws.com/repo.commonsware.com" } + maven { url 'https://s3.amazonaws.com/repo.commonsware.com' } } dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' - implementation 'androidx.core:core-ktx:1.5.0-rc01' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts + implementation 'androidx.core:core-ktx:1.5.0' implementation 'androidx.exifinterface:exifinterface:1.3.2' - implementation "androidx.multidex:multidex:2.0.1" + implementation 'androidx.multidex:multidex:2.0.1' implementation 'com.commonsware.cwac:document:0.4.1' - implementation 'com.drewnoakes:metadata-extractor:2.15.0' + implementation 'com.drewnoakes:metadata-extractor:2.16.0' implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a' // forked, built by JitPack implementation 'com.github.bumptech.glide:glide:4.12.0' diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt index ff76d43b0..b7decb934 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt @@ -29,7 +29,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getEntry" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getEntry) } + "getEntry" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEntry) } "getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getThumbnail) } "getRegion" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getRegion) } "rename" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::rename) } @@ -40,7 +40,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { } } - private suspend fun getEntry(call: MethodCall, result: MethodChannel.Result) { + private fun getEntry(call: MethodCall, result: MethodChannel.Result) { val mimeType = call.argument("mimeType") // MIME type is optional val uri = call.argument("uri")?.let { Uri.parse(it) } if (uri == null) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt index 98f80393f..68811e69b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt @@ -127,16 +127,7 @@ object Metadata { Log.d(LOG_TAG, "use a preview for uri=$uri mimeType=$mimeType size=$sizeBytes") var previewFile = previewFiles[uri] if (previewFile == null) { - previewFile = File.createTempFile("aves", null, context.cacheDir).apply { - deleteOnExit() - outputStream().use { output -> - StorageUtils.openInputStream(context, uri)?.use { input -> - val b = ByteArray(previewSize) - input.read(b, 0, previewSize) - output.write(b) - } - } - } + previewFile = createPreviewFile(context, uri) previewFiles[uri] = previewFile } Uri.fromFile(previewFile) @@ -147,6 +138,19 @@ object Metadata { } } + fun createPreviewFile(context: Context, uri: Uri): File { + return File.createTempFile("aves", null, context.cacheDir).apply { + deleteOnExit() + outputStream().use { output -> + StorageUtils.openInputStream(context, uri)?.use { input -> + val b = ByteArray(previewSize) + input.read(b, 0, previewSize) + output.write(b) + } + } + } + } + fun openSafeInputStream(context: Context, uri: Uri, mimeType: String, sizeBytes: Long?): InputStream? { val safeUri = getSafeUri(context, uri, mimeType, sizeBytes) return StorageUtils.openInputStream(context, safeUri) 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 0f85cc768..03ac091b8 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 @@ -4,10 +4,44 @@ import android.content.Context import android.net.Uri import android.provider.MediaStore import android.provider.OpenableColumns +import android.util.Log +import com.drew.imaging.ImageMetadataReader +import com.drew.metadata.file.FileTypeDirectory +import deckers.thibault.aves.metadata.Metadata +import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString import deckers.thibault.aves.model.SourceEntry +import deckers.thibault.aves.utils.LogUtils +import deckers.thibault.aves.utils.MimeTypes +import deckers.thibault.aves.utils.StorageUtils internal class ContentImageProvider : ImageProvider() { - override fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { + override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) { + // source MIME type may be incorrect, so we get a second opinion if possible + var extractorMimeType: String? = null + try { + val safeUri = Uri.fromFile(Metadata.createPreviewFile(context, uri)) + StorageUtils.openInputStream(context, safeUri)?.use { input -> + val metadata = ImageMetadataReader.readMetadata(input) + for (dir in metadata.getDirectoriesOfType(FileTypeDirectory::class.java)) { + // `metadata-extractor` is the most reliable, except for `tiff` (false positives, false negatives) + // cf https://github.com/drewnoakes/metadata-extractor/issues/296 + dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) { + if (it != MimeTypes.TIFF) { + extractorMimeType = it + if (extractorMimeType != sourceMimeType) { + Log.d(LOG_TAG, "source MIME type is $sourceMimeType but extracted MIME type is $extractorMimeType for uri=$uri") + } + } + } + } + } + } catch (e: Exception) { + 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) + } + + val mimeType = extractorMimeType ?: sourceMimeType if (mimeType == null) { callback.onFailure(Exception("MIME type is null for uri=$uri")) return @@ -39,6 +73,8 @@ internal class ContentImageProvider : ImageProvider() { } companion object { + private val LOG_TAG = LogUtils.createTag() + @Suppress("DEPRECATION") const val PATH = MediaStore.MediaColumns.DATA diff --git a/android/build.gradle b/android/build.gradle index ab25c38eb..cf42cff6e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,21 +1,15 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.4.32' + ext.kotlin_version = '1.5.10' repositories { google() mavenCentral() - // TODO TLAD remove jcenter (migrating to mavenCentral) when this is fixed: https://youtrack.jetbrains.com/issue/IDEA-261387 - jcenter { - content { - includeModule("org.jetbrains.trove4j", "trove4j") - } - } } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:4.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.gms:google-services:4.3.5' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.2' + classpath 'com.google.gms:google-services:4.3.8' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.0' } } @@ -23,12 +17,6 @@ allprojects { repositories { google() mavenCentral() - // TODO TLAD remove jcenter (migrating to mavenCentral) when this is fixed: https://youtrack.jetbrains.com/issue/IDEA-261387 - jcenter { - content { - includeModule("org.jetbrains.trove4j", "trove4j") - } - } } // gradle.projectsEvaluated { // tasks.withType(JavaCompile) { @@ -40,8 +28,6 @@ allprojects { rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { project.evaluationDependsOn(':app') } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index c10917bb8..6d2751c0c 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-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/lib/geo/countries.dart b/lib/geo/countries.dart index dfea76cb9..3c3aff4fc 100644 --- a/lib/geo/countries.dart +++ b/lib/geo/countries.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:aves/geo/topojson.dart'; +import 'package:collection/collection.dart'; import 'package:country_code/country_code.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -13,17 +14,17 @@ class CountryTopology { CountryTopology._private(); - Topology _topology; + Topology? _topology; - Future getTopology() => _topology != null ? SynchronousFuture(_topology) : rootBundle.loadString(topoJsonAsset).then(TopoJson().parse); + Future getTopology() => _topology != null ? SynchronousFuture(_topology) : rootBundle.loadString(topoJsonAsset).then(TopoJson().parse); // returns the country containing given coordinates - Future countryCode(LatLng position) async { + Future countryCode(LatLng position) async { return _countryOfNumeric(await numericCode(position)); } // returns the ISO 3166-1 numeric code of the country containing given coordinates - Future numericCode(LatLng position) async { + Future numericCode(LatLng position) async { final topology = await getTopology(); if (topology == null) return null; @@ -34,21 +35,25 @@ class CountryTopology { // returns a map of the given positions by country Future>> countryCodeMap(Set positions) async { final numericMap = await numericCodeMap(positions); - numericMap.remove(null); - final codeMap = numericMap.map((key, value) { - final code = _countryOfNumeric(key); - return code == null ? null : MapEntry(code, value); - }); - codeMap.remove(null); - return codeMap; + if (numericMap == null) return {}; + + final codeMapEntries = numericMap.entries + .map((kv) { + final code = _countryOfNumeric(kv.key); + return MapEntry(code, kv.value); + }) + .where((kv) => kv.key != null) + .cast>>(); + + return Map.fromEntries(codeMapEntries); } // returns a map of the given positions by the ISO 3166-1 numeric code of the country containing them - Future>> numericCodeMap(Set positions) async { + Future>?> numericCodeMap(Set positions) async { final topology = await getTopology(); if (topology == null) return null; - return compute(_isoNumericCodeMap, _IsoNumericCodeMapData(topology, positions)); + return compute<_IsoNumericCodeMapData, Map>>(_isoNumericCodeMap, _IsoNumericCodeMapData(topology, positions)); } static Future>> _isoNumericCodeMap(_IsoNumericCodeMapData data) async { @@ -58,19 +63,21 @@ class CountryTopology { final byCode = >{}; for (final position in data.positions) { final code = _getNumeric(topology, countries, position); - byCode[code] = (byCode[code] ?? {})..add(position); + if (code != null) { + byCode[code] = (byCode[code] ?? {})..add(position); + } } return byCode; } catch (error, stack) { // an unhandled error in a spawn isolate would make the app crash debugPrint('failed to get country codes with error=$error\n$stack'); } - return null; + return {}; } - static int _getNumeric(Topology topology, List mruCountries, LatLng position) { + static int? _getNumeric(Topology topology, List mruCountries, LatLng position) { final point = [position.longitude, position.latitude]; - final hit = mruCountries.firstWhere((country) => country.containsPoint(topology, point), orElse: () => null); + final hit = mruCountries.firstWhereOrNull((country) => country.containsPoint(topology, point)); if (hit == null) return null; // promote hit countries, assuming given positions are likely to come from the same countries @@ -79,12 +86,12 @@ class CountryTopology { mruCountries.insert(0, hit); } - final idString = (hit.id as String); + final idString = (hit.id as String?); final code = idString == null ? null : int.tryParse(idString); return code; } - static CountryCode _countryOfNumeric(int numeric) { + static CountryCode? _countryOfNumeric(int? numeric) { if (numeric == null) return null; try { return CountryCode.ofNumeric(numeric); diff --git a/lib/geo/format.dart b/lib/geo/format.dart index 517a74c2f..a52dfd192 100644 --- a/lib/geo/format.dart +++ b/lib/geo/format.dart @@ -23,7 +23,6 @@ String _decimal2sexagesimal(final double degDecimal) { // returns coordinates formatted as DMS, e.g. ['41° 24′ 12.2″ N', '2° 10′ 26.5″ E'] List toDMS(LatLng latLng) { - if (latLng == null) return []; final lat = latLng.latitude; final lng = latLng.longitude; return [ diff --git a/lib/geo/topojson.dart b/lib/geo/topojson.dart index 7a4f029b6..5ef57813c 100644 --- a/lib/geo/topojson.dart +++ b/lib/geo/topojson.dart @@ -5,11 +5,11 @@ import 'package:flutter/foundation.dart'; // cf https://github.com/topojson/topojson-specification class TopoJson { - Future parse(String data) async { - return compute(_isoParse, data); + Future parse(String data) async { + return compute(_isoParse, data); } - static Topology _isoParse(String jsonData) { + static Topology? _isoParse(String jsonData) { try { final data = json.decode(jsonData) as Map; return Topology.parse(data); @@ -23,7 +23,7 @@ class TopoJson { enum TopoJsonObjectType { topology, point, multipoint, linestring, multilinestring, polygon, multipolygon, geometrycollection } -TopoJsonObjectType _parseTopoJsonObjectType(String data) { +TopoJsonObjectType? _parseTopoJsonObjectType(String? data) { switch (data) { case 'Topology': return TopoJsonObjectType.topology; @@ -46,7 +46,7 @@ TopoJsonObjectType _parseTopoJsonObjectType(String data) { } class TopologyJsonObject { - final List bbox; + final List? bbox; TopologyJsonObject.parse(Map data) : bbox = data.containsKey('bbox') ? (data['bbox'] as List).cast().toList() : null; } @@ -54,10 +54,19 @@ class TopologyJsonObject { class Topology extends TopologyJsonObject { final Map objects; final List>> arcs; - final Transform transform; + final Transform? transform; Topology.parse(Map data) - : objects = (data['objects'] as Map).cast().map((name, geometry) => MapEntry(name, Geometry.build(geometry))), + : objects = Map.fromEntries((data['objects'] as Map) + .cast() + .entries + .map((kv) { + final name = kv.key; + final geometry = Geometry.build(kv.value); + return geometry != null ? MapEntry(name, geometry) : null; + }) + .where((kv) => kv != null) + .cast>()), arcs = (data['arcs'] as List).cast().map((arc) => arc.cast().map((position) => position.cast()).toList()).toList(), transform = data.containsKey('transform') ? Transform.parse((data['transform'] as Map).cast()) : null, super.parse(data); @@ -69,8 +78,8 @@ class Topology extends TopologyJsonObject { var x = 0, y = 0; arc = arc.map((quantized) { final absolute = List.of(quantized); - absolute[0] = (x += quantized[0]) * transform.scale[0] + transform.translate[0]; - absolute[1] = (y += quantized[1]) * transform.scale[1] + transform.translate[1]; + absolute[0] = (x += quantized[0] as int) * transform!.scale[0] + transform!.translate[0]; + absolute[1] = (y += quantized[1] as int) * transform!.scale[1] + transform!.translate[1]; return absolute; }).toList(); } @@ -126,17 +135,18 @@ class Transform { abstract class Geometry extends TopologyJsonObject { final dynamic id; - final Map properties; + final Map? properties; Geometry.parse(Map data) : id = data.containsKey('id') ? data['id'] : null, - properties = data.containsKey('properties') ? data['properties'] as Map : null, + properties = data.containsKey('properties') ? data['properties'] as Map? : null, super.parse(data); - static Geometry build(Map data) { - final type = _parseTopoJsonObjectType(data['type'] as String); + static Geometry? build(Map data) { + final type = _parseTopoJsonObjectType(data['type'] as String?); switch (type) { case TopoJsonObjectType.topology: + case null: return null; case TopoJsonObjectType.point: return Point.parse(data); @@ -153,7 +163,6 @@ abstract class Geometry extends TopologyJsonObject { case TopoJsonObjectType.geometrycollection: return GeometryCollection.parse(data); } - return null; } bool containsPoint(Topology topology, List point) => false; @@ -198,11 +207,11 @@ class Polygon extends Geometry { : arcs = (data['arcs'] as List).cast().map((arc) => arc.cast()).toList(), super.parse(data); - List>> _rings; + List>>? _rings; List>> rings(Topology topology) { _rings ??= topology._decodePolygonArcs(arcs); - return _rings; + return _rings!; } @override @@ -218,11 +227,11 @@ class MultiPolygon extends Geometry { : arcs = (data['arcs'] as List).cast().map((polygon) => polygon.cast().map((arc) => arc.cast()).toList()).toList(), super.parse(data); - List>>> _polygons; + List>>>? _polygons; List>>> polygons(Topology topology) { _polygons ??= topology._decodeMultiPolygonArcs(arcs); - return _polygons; + return _polygons!; } @override @@ -235,7 +244,7 @@ class GeometryCollection extends Geometry { final List geometries; GeometryCollection.parse(Map data) - : geometries = (data['geometries'] as List).cast>().map(Geometry.build).toList(), + : geometries = (data['geometries'] as List).cast>().map(Geometry.build).where((geometry) => geometry != null).cast().toList(), super.parse(data); @override diff --git a/lib/image_providers/app_icon_image_provider.dart b/lib/image_providers/app_icon_image_provider.dart index cbc7490e5..a51ea806c 100644 --- a/lib/image_providers/app_icon_image_provider.dart +++ b/lib/image_providers/app_icon_image_provider.dart @@ -6,11 +6,10 @@ import 'package:flutter/widgets.dart'; class AppIconImage extends ImageProvider { const AppIconImage({ - @required this.packageName, - @required this.size, + required this.packageName, + required this.size, this.scale = 1.0, - }) : assert(packageName != null), - assert(scale != null); + }); final String packageName; final double size; @@ -39,7 +38,7 @@ class AppIconImage extends ImageProvider { Future _loadAsync(AppIconImageKey key, DecoderCallback decode) async { try { final bytes = await AndroidAppService.getAppIcon(key.packageName, key.size); - if (bytes == null) { + if (bytes.isEmpty) { throw StateError('$packageName app icon loading failed'); } return await decode(bytes); @@ -56,9 +55,9 @@ class AppIconImageKey { final double scale; const AppIconImageKey({ - @required this.packageName, - @required this.size, - this.scale, + required this.packageName, + required this.size, + this.scale = 1.0, }); @override diff --git a/lib/image_providers/region_provider.dart b/lib/image_providers/region_provider.dart index 136bcc626..1808b8e8a 100644 --- a/lib/image_providers/region_provider.dart +++ b/lib/image_providers/region_provider.dart @@ -9,7 +9,7 @@ import 'package:flutter/widgets.dart'; class RegionProvider extends ImageProvider { final RegionProviderKey key; - RegionProvider(this.key) : assert(key != null); + RegionProvider(this.key); @override Future obtainKey(ImageConfiguration configuration) { @@ -43,7 +43,7 @@ class RegionProvider extends ImageProvider { pageId: pageId, taskKey: key, ); - if (bytes == null) { + if (bytes.isEmpty) { throw StateError('$uri ($mimeType) region loading failed'); } return await decode(bytes); @@ -66,30 +66,24 @@ class RegionProviderKey { // do not store the entry as it is, because the key should be constant // but the entry attributes may change over time final String uri, mimeType; - final int pageId, rotationDegrees, sampleSize; + final int? pageId; + final int rotationDegrees, sampleSize; final bool isFlipped; final Rectangle region; final Size imageSize; final double scale; const RegionProviderKey({ - @required this.uri, - @required this.mimeType, - @required this.pageId, - @required this.rotationDegrees, - @required this.isFlipped, - @required this.sampleSize, - @required this.region, - @required this.imageSize, + required this.uri, + required this.mimeType, + required this.pageId, + required this.rotationDegrees, + required this.isFlipped, + required this.sampleSize, + required this.region, + required this.imageSize, this.scale = 1.0, - }) : assert(uri != null), - assert(mimeType != null), - assert(rotationDegrees != null), - assert(isFlipped != null), - assert(sampleSize != null), - assert(region != null), - assert(imageSize != null), - assert(scale != null); + }); @override bool operator ==(Object other) { diff --git a/lib/image_providers/thumbnail_provider.dart b/lib/image_providers/thumbnail_provider.dart index 0578b63e3..309f696cc 100644 --- a/lib/image_providers/thumbnail_provider.dart +++ b/lib/image_providers/thumbnail_provider.dart @@ -7,7 +7,7 @@ import 'package:flutter/widgets.dart'; class ThumbnailProvider extends ImageProvider { final ThumbnailProviderKey key; - ThumbnailProvider(this.key) : assert(key != null); + ThumbnailProvider(this.key); @override Future obtainKey(ImageConfiguration configuration) { @@ -22,6 +22,7 @@ class ThumbnailProvider extends ImageProvider { return MultiFrameImageStreamCompleter( codec: _loadAsync(key, decode), scale: 1.0, + debugLabel: kReleaseMode ? null : [key.uri, key.extent].join('-'), informationCollector: () sync* { yield ErrorDescription('uri=${key.uri}, pageId=${key.pageId}, mimeType=${key.mimeType}, extent=${key.extent}'); }, @@ -43,7 +44,7 @@ class ThumbnailProvider extends ImageProvider { extent: key.extent, taskKey: key, ); - if (bytes == null) { + if (bytes.isEmpty) { throw StateError('$uri ($mimeType) loading failed'); } return await decode(bytes); @@ -66,25 +67,21 @@ class ThumbnailProviderKey { // do not store the entry as it is, because the key should be constant // but the entry attributes may change over time final String uri, mimeType; - final int pageId, rotationDegrees; + final int? pageId; + final int rotationDegrees; final bool isFlipped; final int dateModifiedSecs; final double extent; const ThumbnailProviderKey({ - @required this.uri, - @required this.mimeType, - @required this.pageId, - @required this.rotationDegrees, - @required this.isFlipped, - @required this.dateModifiedSecs, + required this.uri, + required this.mimeType, + required this.pageId, + required this.rotationDegrees, + required this.isFlipped, + required this.dateModifiedSecs, this.extent = 0, - }) : assert(uri != null), - assert(mimeType != null), - assert(rotationDegrees != null), - assert(isFlipped != null), - assert(dateModifiedSecs != null), - assert(extent != null); + }); @override bool operator ==(Object other) { diff --git a/lib/image_providers/uri_image_provider.dart b/lib/image_providers/uri_image_provider.dart index c6b1c31fa..80ea53f3d 100644 --- a/lib/image_providers/uri_image_provider.dart +++ b/lib/image_providers/uri_image_provider.dart @@ -8,20 +8,19 @@ import 'package:pedantic/pedantic.dart'; class UriImage extends ImageProvider { final String uri, mimeType; - final int pageId, rotationDegrees, expectedContentLength; + final int? pageId, rotationDegrees, expectedContentLength; final bool isFlipped; final double scale; const UriImage({ - @required this.uri, - @required this.mimeType, - @required this.pageId, - @required this.rotationDegrees, - @required this.isFlipped, + required this.uri, + required this.mimeType, + required this.pageId, + required this.rotationDegrees, + required this.isFlipped, this.expectedContentLength, this.scale = 1.0, - }) : assert(uri != null), - assert(scale != null); + }); @override Future obtainKey(ImageConfiguration configuration) { @@ -60,7 +59,7 @@ class UriImage extends ImageProvider { )); }, ); - if (bytes == null) { + if (bytes.isEmpty) { throw StateError('$uri ($mimeType) loading failed'); } return await decode(bytes); diff --git a/lib/image_providers/uri_picture_provider.dart b/lib/image_providers/uri_picture_provider.dart index 2f4071c8c..8a13e7125 100644 --- a/lib/image_providers/uri_picture_provider.dart +++ b/lib/image_providers/uri_picture_provider.dart @@ -2,15 +2,13 @@ import 'package:aves/services/services.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:pedantic/pedantic.dart'; class UriPicture extends PictureProvider { const UriPicture({ - @required this.uri, - @required this.mimeType, - ColorFilter colorFilter, - }) : assert(uri != null), - super(colorFilter); + required this.uri, + required this.mimeType, + ColorFilter? colorFilter, + }) : super(colorFilter); final String uri, mimeType; @@ -20,25 +18,30 @@ class UriPicture extends PictureProvider { } @override - PictureStreamCompleter load(UriPicture key, {PictureErrorListener onError}) { + PictureStreamCompleter load(UriPicture key, {PictureErrorListener? onError}) { return OneFramePictureStreamCompleter(_loadAsync(key, onError: onError), informationCollector: () sync* { yield DiagnosticsProperty('uri', uri); }); } - Future _loadAsync(UriPicture key, {PictureErrorListener onError}) async { + Future _loadAsync(UriPicture key, {PictureErrorListener? onError}) async { assert(key == this); final data = await imageFileService.getSvg(uri, mimeType); - if (data == null || data.isEmpty) { + if (data.isEmpty) { return null; } final decoder = SvgPicture.svgByteDecoder; if (onError != null) { - final future = decoder(data, colorFilter, key.toString()); - unawaited(future.catchError(onError)); - return future; + return decoder( + data, + colorFilter, + key.toString(), + ).catchError((error, stack) async { + onError(error, stack); + return Future.error(error, stack); + }); } return decoder(data, colorFilter, key.toString()); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5cbeedb7d..b97799115 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -18,6 +18,8 @@ "@applyButtonLabel": {}, "deleteButtonLabel": "DELETE", "@deleteButtonLabel": {}, + "showButtonLabel": "SHOW", + "@showButtonLabel": {}, "hideButtonLabel": "HIDE", "@hideButtonLabel": {}, "continueButtonLabel": "CONTINUE", @@ -555,9 +557,9 @@ "@settingsVideoEnableHardwareAcceleration": {}, "settingsVideoEnableAutoPlay": "Auto play", "@settingsVideoEnableAutoPlay": {}, - "settingsVideoLoopModeTile": "Loop mode", + "settingsVideoLoopModeTile": "Loop mode", "@settingsVideoLoopModeTile": {}, - "settingsVideoLoopModeTitle": "Loop Mode", + "settingsVideoLoopModeTitle": "Loop Mode", "@settingsVideoLoopModeTitle": {}, "settingsSectionPrivacy": "Privacy", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 391f5e0bd..4a5cb44dc 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -7,6 +7,7 @@ "applyButtonLabel": "확인", "deleteButtonLabel": "삭제", + "showButtonLabel": "보기", "hideButtonLabel": "숨기기", "continueButtonLabel": "다음", "clearTooltip": "초기화", @@ -259,8 +260,8 @@ "settingsVideoShowVideos": "미디어에 동영상 표시", "settingsVideoEnableHardwareAcceleration": "하드웨어 가속", "settingsVideoEnableAutoPlay": "자동 재생", - "settingsVideoLoopModeTile": "반복 모드", - "settingsVideoLoopModeTitle": "반복 모드", + "settingsVideoLoopModeTile": "반복 모드", + "settingsVideoLoopModeTitle": "반복 모드", "settingsSectionPrivacy": "개인정보 보호", "settingsEnableAnalytics": "진단 데이터 보내기", diff --git a/lib/main.dart b/lib/main.dart index 85a98d606..470a5c629 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,36 +1,26 @@ +// @dart=2.9 import 'dart:isolate'; -import 'dart:ui'; -import 'package:aves/app_mode.dart'; -import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/model/source/media_store_source.dart'; -import 'package:aves/services/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/behaviour/route_tracker.dart'; -import 'package:aves/widgets/common/behaviour/routes.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; -import 'package:aves/widgets/home_page.dart'; -import 'package:aves/widgets/welcome_page.dart'; -import 'package:firebase_analytics/firebase_analytics.dart'; -import 'package:firebase_analytics/observer.dart'; -import 'package:firebase_core/firebase_core.dart'; +import 'package:aves/widgets/aves_app.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:overlay_support/overlay_support.dart'; -import 'package:provider/provider.dart'; void main() { // HttpClient.enableTimelineLogging = true; // enable network traffic logging // debugPrintGestureArenaDiagnostics = true; +// Invert oversized images (debug mode only) +// cf https://flutter.dev/docs/development/tools/devtools/inspector +// but unaware of device pixel ratio as of Flutter 2.2.1: https://github.com/flutter/flutter/issues/76208 +// +// MaterialApp.checkerboardOffscreenLayers +// cf https://flutter.dev/docs/perf/rendering/ui-performance#checking-for-offscreen-layers +// +// MaterialApp.checkerboardRasterCacheImages +// cf https://flutter.dev/docs/perf/rendering/ui-performance#checking-for-non-cached-images +// +// flutter run --profile --trace-skia + Isolate.current.addErrorListener(RawReceivePort((pair) async { final List errorAndStacktrace = pair; await FirebaseCrashlytics.instance.recordError( @@ -41,147 +31,3 @@ void main() { runApp(AvesApp()); } - -class AvesApp extends StatefulWidget { - @override - _AvesAppState createState() => _AvesAppState(); -} - -class _AvesAppState extends State { - final ValueNotifier appModeNotifier = ValueNotifier(AppMode.main); - Future _appSetup; - final _mediaStoreSource = MediaStoreSource(); - final Debouncer _contentChangeDebouncer = Debouncer(delay: Durations.contentChangeDebounceDelay); - final Set changedUris = {}; - - // observers are not registered when using the same list object with different items - // the list itself needs to be reassigned - List _navigatorObservers = []; - final EventChannel _contentChangeChannel = EventChannel('deckers.thibault/aves/contentchange'); - final EventChannel _newIntentChannel = EventChannel('deckers.thibault/aves/intent'); - final GlobalKey _navigatorKey = GlobalKey(debugLabel: 'app-navigator'); - - Widget getFirstPage({Map intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : WelcomePage(); - - @override - void initState() { - super.initState(); - initPlatformServices(); - _appSetup = _setup(); - _contentChangeChannel.receiveBroadcastStream().listen((event) => _onContentChange(event as String)); - _newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map)); - } - - @override - Widget build(BuildContext context) { - // place the settings provider above `MaterialApp` - // so it can be used during navigation transitions - return ChangeNotifierProvider.value( - value: settings, - child: ListenableProvider>.value( - value: appModeNotifier, - child: Provider.value( - value: _mediaStoreSource, - child: HighlightInfoProvider( - child: OverlaySupport( - child: FutureBuilder( - future: _appSetup, - builder: (context, snapshot) { - final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done; - final home = initialized - ? getFirstPage() - : Scaffold( - body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox(), - ); - return Selector( - selector: (context, s) => s.locale, - builder: (context, settingsLocale, child) { - return MaterialApp( - navigatorKey: _navigatorKey, - home: home, - navigatorObservers: _navigatorObservers, - onGenerateTitle: (context) => context.l10n.appName, - darkTheme: Themes.darkTheme, - themeMode: ThemeMode.dark, - locale: settingsLocale, - localizationsDelegates: [ - ...AppLocalizations.localizationsDelegates, - ], - supportedLocales: AppLocalizations.supportedLocales, - ); - }); - }, - ), - ), - ), - ), - ), - ); - } - - Widget _buildError(Object error) { - return Container( - alignment: Alignment.center, - padding: EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(AIcons.error), - SizedBox(height: 16), - Text(error.toString()), - ], - ), - ); - } - - Future _setup() async { - await Firebase.initializeApp().then((app) { - final crashlytics = FirebaseCrashlytics.instance; - FlutterError.onError = crashlytics.recordFlutterError; - crashlytics.setCustomKey('locales', window.locales.join(', ')); - final now = DateTime.now(); - crashlytics.setCustomKey('timezone', '${now.timeZoneName} (${now.timeZoneOffset})'); - crashlytics.setCustomKey( - 'build_mode', - kReleaseMode - ? 'release' - : kProfileMode - ? 'profile' - : 'debug'); - }); - await settings.init(); - await settings.initFirebase(); - _navigatorObservers = [ - FirebaseAnalyticsObserver(analytics: FirebaseAnalytics()), - CrashlyticsRouteTracker(), - ]; - } - - void _onNewIntent(Map intentData) { - debugPrint('$runtimeType onNewIntent with intentData=$intentData'); - - // do not reset when relaunching the app - if (appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) return; - - FirebaseCrashlytics.instance.log('New intent'); - _navigatorKey.currentState.pushReplacement(DirectMaterialPageRoute( - settings: RouteSettings(name: HomePage.routeName), - builder: (_) => getFirstPage(intentData: intentData), - )); - } - - void _onContentChange(String uri) { - if (uri != null) changedUris.add(uri); - if (changedUris.isNotEmpty) { - _contentChangeDebouncer(() async { - final todo = changedUris.toSet(); - changedUris.clear(); - final tempUris = await _mediaStoreSource.refreshUris(todo); - if (tempUris.isNotEmpty) { - changedUris.addAll(tempUris); - _onContentChange(null); - } - }); - } - } -} diff --git a/lib/model/actions/chip_actions.dart b/lib/model/actions/chip_actions.dart index 412f745a1..7e531c168 100644 --- a/lib/model/actions/chip_actions.dart +++ b/lib/model/actions/chip_actions.dart @@ -42,7 +42,6 @@ extension ExtraChipAction on ChipAction { case ChipAction.setCover: return context.l10n.chipActionSetCover; } - return null; } IconData getIcon() { @@ -65,6 +64,5 @@ extension ExtraChipAction on ChipAction { case ChipAction.setCover: return AIcons.setCover; } - return null; } } diff --git a/lib/model/actions/entry_actions.dart b/lib/model/actions/entry_actions.dart index 04e4865e1..f709c0e8d 100644 --- a/lib/model/actions/entry_actions.dart +++ b/lib/model/actions/entry_actions.dart @@ -91,10 +91,9 @@ extension ExtraEntryAction on EntryAction { case EntryAction.debug: return 'Debug'; } - return null; } - IconData getIcon() { + IconData? getIcon() { switch (this) { // in app actions case EntryAction.toggleFavourite: @@ -129,6 +128,5 @@ extension ExtraEntryAction on EntryAction { case EntryAction.debug: return AIcons.debug; } - return null; } } diff --git a/lib/model/availability.dart b/lib/model/availability.dart index 43c983f23..828fda49e 100644 --- a/lib/model/availability.dart +++ b/lib/model/availability.dart @@ -1,11 +1,11 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; -import 'package:connectivity/connectivity.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:github/github.dart'; import 'package:google_api_availability/google_api_availability.dart'; -import 'package:package_info/package_info.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:version/version.dart'; abstract class AvesAvailability { @@ -21,7 +21,7 @@ abstract class AvesAvailability { } class LiveAvesAvailability implements AvesAvailability { - bool _isConnected, _hasPlayServices, _isNewVersionAvailable; + bool? _isConnected, _hasPlayServices, _isNewVersionAvailable; LiveAvesAvailability() { Connectivity().onConnectivityChanged.listen(_updateConnectivityFromResult); @@ -32,10 +32,10 @@ class LiveAvesAvailability implements AvesAvailability { @override Future get isConnected async { - if (_isConnected != null) return SynchronousFuture(_isConnected); + if (_isConnected != null) return SynchronousFuture(_isConnected!); final result = await (Connectivity().checkConnectivity()); _updateConnectivityFromResult(result); - return _isConnected; + return _isConnected!; } void _updateConnectivityFromResult(ConnectivityResult result) { @@ -48,11 +48,11 @@ class LiveAvesAvailability implements AvesAvailability { @override Future get hasPlayServices async { - if (_hasPlayServices != null) return SynchronousFuture(_hasPlayServices); + if (_hasPlayServices != null) return SynchronousFuture(_hasPlayServices!); final result = await GoogleApiAvailability.instance.checkGooglePlayServicesAvailability(); _hasPlayServices = result == GooglePlayServicesAvailability.success; debugPrint('Device has Play Services=$_hasPlayServices'); - return _hasPlayServices; + return _hasPlayServices!; } // local geocoding with `geocoder` requires Play Services @@ -61,27 +61,27 @@ class LiveAvesAvailability implements AvesAvailability { @override Future get isNewVersionAvailable async { - if (_isNewVersionAvailable != null) return SynchronousFuture(_isNewVersionAvailable); + if (_isNewVersionAvailable != null) return SynchronousFuture(_isNewVersionAvailable!); final now = DateTime.now(); final dueDate = settings.lastVersionCheckDate.add(Durations.lastVersionCheckInterval); if (now.isBefore(dueDate)) { _isNewVersionAvailable = false; - return SynchronousFuture(_isNewVersionAvailable); + return SynchronousFuture(_isNewVersionAvailable!); } if (!(await isConnected)) return false; Version version(String s) => Version.parse(s.replaceFirst('v', '')); final currentTag = (await PackageInfo.fromPlatform()).version; - final latestTag = (await GitHub().repositories.getLatestRelease(RepositorySlug('deckerst', 'aves'))).tagName; + final latestTag = (await GitHub().repositories.getLatestRelease(RepositorySlug('deckerst', 'aves'))).tagName!; _isNewVersionAvailable = version(latestTag) > version(currentTag); - if (_isNewVersionAvailable) { + if (_isNewVersionAvailable!) { debugPrint('Aves $latestTag is available on github'); } else { debugPrint('Aves $currentTag is the latest version'); settings.lastVersionCheckDate = now; } - return _isNewVersionAvailable; + return _isNewVersionAvailable!; } } diff --git a/lib/model/covers.dart b/lib/model/covers.dart index 5a083438a..ffebf4b98 100644 --- a/lib/model/covers.dart +++ b/lib/model/covers.dart @@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/services/services.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -18,19 +19,19 @@ class Covers with ChangeNotifier { int get count => _rows.length; - int coverContentId(CollectionFilter filter) => _rows.firstWhere((row) => row.filter == filter, orElse: () => null)?.contentId; + int? coverContentId(CollectionFilter filter) => _rows.firstWhereOrNull((row) => row.filter == filter)?.contentId; - Future set(CollectionFilter filter, int contentId) async { + Future set(CollectionFilter filter, int? contentId) async { // erase contextual properties from filters before saving them if (filter is AlbumFilter) { - filter = AlbumFilter((filter as AlbumFilter).album, null); + filter = AlbumFilter(filter.album, null); } - final row = CoverRow(filter: filter, contentId: contentId); _rows.removeWhere((row) => row.filter == filter); if (contentId == null) { - await metadataDb.removeCovers({row}); + await metadataDb.removeCovers({filter}); } else { + final row = CoverRow(filter: filter, contentId: contentId); _rows.add(row); await metadataDb.addCovers({row}); } @@ -46,11 +47,11 @@ class Covers with ChangeNotifier { final filter = oldRow.filter; _rows.remove(oldRow); if (filter.test(entry)) { - final newRow = CoverRow(filter: filter, contentId: entry.contentId); + final newRow = CoverRow(filter: filter, contentId: entry.contentId!); await metadataDb.updateCoverEntryId(oldRow.contentId, newRow); _rows.add(newRow); } else { - await metadataDb.removeCovers({oldRow}); + await metadataDb.removeCovers({filter}); } } @@ -61,7 +62,7 @@ class Covers with ChangeNotifier { final contentIds = entries.map((entry) => entry.contentId).toSet(); final removedRows = _rows.where((row) => contentIds.contains(row.contentId)).toSet(); - await metadataDb.removeCovers(removedRows); + await metadataDb.removeCovers(removedRows.map((row) => row.filter).toSet()); _rows.removeAll(removedRows); notifyListeners(); @@ -81,13 +82,15 @@ class CoverRow { final int contentId; const CoverRow({ - @required this.filter, - @required this.contentId, + required this.filter, + required this.contentId, }); - factory CoverRow.fromMap(Map map) { + static CoverRow? fromMap(Map map) { + final filter = CollectionFilter.fromJson(map['filter']); + if (filter == null) return null; return CoverRow( - filter: CollectionFilter.fromJson(map['filter']), + filter: filter, contentId: map['contentId'], ); } diff --git a/lib/model/entry.dart b/lib/model/entry.dart index e2aaccb58..9a78b309c 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -22,22 +22,22 @@ import '../ref/mime_types.dart'; class AvesEntry { String uri; - String _path, _directory, _filename, _extension; - int pageId, contentId; + String? _path, _directory, _filename, _extension; + int? pageId, contentId; final String sourceMimeType; int width; int height; int sourceRotationDegrees; - final int sizeBytes; - String _sourceTitle; + final int? sizeBytes; + String? _sourceTitle; // `dateModifiedSecs` can be missing in viewer mode - int _dateModifiedSecs; - final int sourceDateTakenMillis; - final int durationMillis; - int _catalogDateMillis; - CatalogMetadata _catalogMetadata; - AddressDetails _addressDetails; + int? _dateModifiedSecs; + final int? sourceDateTakenMillis; + final int? durationMillis; + int? _catalogDateMillis; + CatalogMetadata? _catalogMetadata; + AddressDetails? _addressDetails; final AChangeNotifier imageChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier(); @@ -51,21 +51,20 @@ class AvesEntry { ]; AvesEntry({ - this.uri, - String path, - this.contentId, - this.pageId, - this.sourceMimeType, - @required this.width, - @required this.height, - this.sourceRotationDegrees, - this.sizeBytes, - String sourceTitle, - int dateModifiedSecs, - this.sourceDateTakenMillis, - this.durationMillis, - }) : assert(width != null), - assert(height != null) { + required this.uri, + required String? path, + required this.contentId, + required this.pageId, + required this.sourceMimeType, + required this.width, + required this.height, + required this.sourceRotationDegrees, + required this.sizeBytes, + required String? sourceTitle, + required int? dateModifiedSecs, + required this.sourceDateTakenMillis, + required this.durationMillis, + }) { this.path = path; this.sourceTitle = sourceTitle; this.dateModifiedSecs = dateModifiedSecs; @@ -76,16 +75,17 @@ class AvesEntry { bool get canHaveAlpha => MimeTypes.alphaImages.contains(mimeType); AvesEntry copyWith({ - String uri, - String path, - int contentId, - int dateModifiedSecs, + String? uri, + String? path, + int? contentId, + int? dateModifiedSecs, }) { final copyContentId = contentId ?? this.contentId; final copied = AvesEntry( uri: uri ?? this.uri, path: path ?? this.path, contentId: copyContentId, + pageId: null, sourceMimeType: sourceMimeType, width: width, height: height, @@ -106,17 +106,18 @@ class AvesEntry { factory AvesEntry.fromMap(Map map) { return AvesEntry( uri: map['uri'] as String, - path: map['path'] as String, - contentId: map['contentId'] as int, + path: map['path'] as String?, + pageId: null, + contentId: map['contentId'] as int?, sourceMimeType: map['sourceMimeType'] as String, - width: map['width'] as int ?? 0, - height: map['height'] as int ?? 0, - sourceRotationDegrees: map['sourceRotationDegrees'] as int ?? 0, - sizeBytes: map['sizeBytes'] as int, - sourceTitle: map['title'] as String, - dateModifiedSecs: map['dateModifiedSecs'] as int, - sourceDateTakenMillis: map['sourceDateTakenMillis'] as int, - durationMillis: map['durationMillis'] as int, + width: map['width'] as int? ?? 0, + height: map['height'] as int? ?? 0, + sourceRotationDegrees: map['sourceRotationDegrees'] as int? ?? 0, + sizeBytes: map['sizeBytes'] as int?, + sourceTitle: map['title'] as String?, + dateModifiedSecs: map['dateModifiedSecs'] as int?, + sourceDateTakenMillis: map['sourceDateTakenMillis'] as int?, + durationMillis: map['durationMillis'] as int?, ); } @@ -150,27 +151,27 @@ class AvesEntry { @override String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, path=$path, pageId=$pageId}'; - set path(String path) { + set path(String? path) { _path = path; _directory = null; _filename = null; _extension = null; } - String get path => _path; + String? get path => _path; - String get directory { - _directory ??= path != null ? pContext.dirname(path) : null; + String? get directory { + _directory ??= path != null ? pContext.dirname(path!) : null; return _directory; } - String get filenameWithoutExtension { - _filename ??= path != null ? pContext.basenameWithoutExtension(path) : null; + String? get filenameWithoutExtension { + _filename ??= path != null ? pContext.basenameWithoutExtension(path!) : null; return _filename; } - String get extension { - _extension ??= path != null ? pContext.extension(path) : null; + String? get extension { + _extension ??= path != null ? pContext.extension(path!) : null; return _extension; } @@ -258,16 +259,16 @@ class AvesEntry { static const ratioSeparator = '\u2236'; static const resolutionSeparator = ' \u00D7 '; - bool get isSized => (width ?? 0) > 0 && (height ?? 0) > 0; + bool get isSized => width > 0 && height > 0; String get resolutionText { - final ws = width ?? '?'; - final hs = height ?? '?'; + final ws = width; + final hs = height; return isRotated ? '$hs$resolutionSeparator$ws' : '$ws$resolutionSeparator$hs'; } String get aspectRatioText { - if (width != null && height != null && width > 0 && height > 0) { + if (width > 0 && height > 0) { final gcd = width.gcd(height); final w = width ~/ gcd; final h = height ~/ gcd; @@ -288,24 +289,36 @@ class AvesEntry { return isRotated ? Size(h, w) : Size(w, h); } - int get megaPixels => width != null && height != null ? (width * height / 1000000).round() : null; + Size videoDisplaySize(double sar) { + final size = displaySize; + if (sar != 1) { + final dar = displayAspectRatio * sar; + final w = size.width; + final h = size.height; + if (w >= h) return Size(w, w / dar); + if (h > w) return Size(h * dar, h); + } + return size; + } - DateTime _bestDate; + int get megaPixels => (width * height / 1000000).round(); - DateTime get bestDate { + DateTime? _bestDate; + + DateTime? get bestDate { if (_bestDate == null) { if ((_catalogDateMillis ?? 0) > 0) { - _bestDate = DateTime.fromMillisecondsSinceEpoch(_catalogDateMillis); + _bestDate = DateTime.fromMillisecondsSinceEpoch(_catalogDateMillis!); } else if ((sourceDateTakenMillis ?? 0) > 0) { - _bestDate = DateTime.fromMillisecondsSinceEpoch(sourceDateTakenMillis); + _bestDate = DateTime.fromMillisecondsSinceEpoch(sourceDateTakenMillis!); } else if ((dateModifiedSecs ?? 0) > 0) { - _bestDate = DateTime.fromMillisecondsSinceEpoch(dateModifiedSecs * 1000); + _bestDate = DateTime.fromMillisecondsSinceEpoch(dateModifiedSecs! * 1000); } } return _bestDate; } - int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees ?? 0; + int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees; set rotationDegrees(int rotationDegrees) { sourceRotationDegrees = rotationDegrees; @@ -316,78 +329,78 @@ class AvesEntry { set isFlipped(bool isFlipped) => _catalogMetadata?.isFlipped = isFlipped; - String get sourceTitle => _sourceTitle; + String? get sourceTitle => _sourceTitle; - set sourceTitle(String sourceTitle) { + set sourceTitle(String? sourceTitle) { _sourceTitle = sourceTitle; _bestTitle = null; } - int get dateModifiedSecs => _dateModifiedSecs; + int? get dateModifiedSecs => _dateModifiedSecs; - set dateModifiedSecs(int dateModifiedSecs) { + set dateModifiedSecs(int? dateModifiedSecs) { _dateModifiedSecs = dateModifiedSecs; _bestDate = null; } - DateTime get monthTaken { + DateTime? get monthTaken { final d = bestDate; return d == null ? null : DateTime(d.year, d.month); } - DateTime get dayTaken { + DateTime? get dayTaken { final d = bestDate; return d == null ? null : DateTime(d.year, d.month, d.day); } - String _durationText; + String? _durationText; String get durationText { _durationText ??= formatFriendlyDuration(Duration(milliseconds: durationMillis ?? 0)); - return _durationText; + return _durationText!; } // returns whether this entry has GPS coordinates // (0, 0) coordinates are considered invalid, as it is likely a default value - bool get hasGps => _catalogMetadata != null && _catalogMetadata.latitude != null && _catalogMetadata.longitude != null && (_catalogMetadata.latitude != 0 || _catalogMetadata.longitude != 0); + bool get hasGps => (_catalogMetadata?.latitude ?? 0) != 0 || (_catalogMetadata?.longitude ?? 0) != 0; bool get hasAddress => _addressDetails != null; // has a place, or at least the full country name // derived from Google reverse geocoding addresses - bool get hasFineAddress => _addressDetails != null && (_addressDetails.place?.isNotEmpty == true || (_addressDetails.countryName?.length ?? 0) > 3); + bool get hasFineAddress => _addressDetails?.place?.isNotEmpty == true || (_addressDetails?.countryName?.length ?? 0) > 3; - LatLng get latLng => hasGps ? LatLng(_catalogMetadata.latitude, _catalogMetadata.longitude) : null; + LatLng? get latLng => hasGps ? LatLng(_catalogMetadata!.latitude!, _catalogMetadata!.longitude!) : null; - String get geoUri { + String? get geoUri { if (!hasGps) return null; - final latitude = roundToPrecision(_catalogMetadata.latitude, decimals: 6); - final longitude = roundToPrecision(_catalogMetadata.longitude, decimals: 6); + final latitude = roundToPrecision(_catalogMetadata!.latitude!, decimals: 6); + final longitude = roundToPrecision(_catalogMetadata!.longitude!, decimals: 6); return 'geo:$latitude,$longitude?q=$latitude,$longitude'; } - List _xmpSubjects; + List? _xmpSubjects; List get xmpSubjects { - _xmpSubjects ??= _catalogMetadata?.xmpSubjects?.split(';')?.where((tag) => tag.isNotEmpty)?.toList() ?? []; - return _xmpSubjects; + _xmpSubjects ??= _catalogMetadata?.xmpSubjects?.split(';').where((tag) => tag.isNotEmpty).toList() ?? []; + return _xmpSubjects!; } - String _bestTitle; + String? _bestTitle; - String get bestTitle { - _bestTitle ??= _catalogMetadata?.xmpTitleDescription?.isNotEmpty == true ? _catalogMetadata.xmpTitleDescription : sourceTitle; + String? get bestTitle { + _bestTitle ??= _catalogMetadata?.xmpTitleDescription?.isNotEmpty == true ? _catalogMetadata!.xmpTitleDescription : sourceTitle; return _bestTitle; } - CatalogMetadata get catalogMetadata => _catalogMetadata; + CatalogMetadata? get catalogMetadata => _catalogMetadata; - set catalogDateMillis(int dateMillis) { + set catalogDateMillis(int? dateMillis) { _catalogDateMillis = dateMillis; _bestDate = null; } - set catalogMetadata(CatalogMetadata newMetadata) { + set catalogMetadata(CatalogMetadata? newMetadata) { final oldDateModifiedSecs = dateModifiedSecs; final oldRotationDegrees = rotationDegrees; final oldIsFlipped = isFlipped; @@ -424,14 +437,14 @@ class AvesEntry { } } - AddressDetails get addressDetails => _addressDetails; + AddressDetails? get addressDetails => _addressDetails; - set addressDetails(AddressDetails newAddress) { + set addressDetails(AddressDetails? newAddress) { _addressDetails = newAddress; addressChangeNotifier.notifyListeners(); } - Future locate({@required bool background}) async { + Future locate({required bool background}) async { if (!hasGps) return; await _locateCountry(); if (await availability.canLocatePlaces) { @@ -442,11 +455,11 @@ class AvesEntry { // quick reverse geocoding to find the country, using an offline asset Future _locateCountry() async { if (!hasGps || hasAddress) return; - final countryCode = await countryTopology.countryCode(latLng); + final countryCode = await countryTopology.countryCode(latLng!); setCountry(countryCode); } - void setCountry(CountryCode countryCode) { + void setCountry(CountryCode? countryCode) { if (hasFineAddress || countryCode == null) return; addressDetails = AddressDetails( contentId: contentId, @@ -455,25 +468,25 @@ class AvesEntry { ); } - String _geocoderLocale; + String? _geocoderLocale; String get geocoderLocale { - _geocoderLocale ??= (settings.locale ?? WidgetsBinding.instance.window.locale).toString(); - return _geocoderLocale; + _geocoderLocale ??= (settings.locale ?? WidgetsBinding.instance!.window.locale).toString(); + return _geocoderLocale!; } // full reverse geocoding, requiring Play Services and some connectivity - Future locatePlace({@required bool background}) async { + Future locatePlace({required bool background}) async { if (!hasGps || hasFineAddress) return; try { - Future> call() => GeocodingService.getAddress(latLng, geocoderLocale); + Future> call() => GeocodingService.getAddress(latLng!, geocoderLocale); final addresses = await (background ? servicePolicy.call( call, priority: ServiceCallPriority.getLocation, ) : call()); - if (addresses != null && addresses.isNotEmpty) { + if (addresses.isNotEmpty) { final address = addresses.first; final cc = address.countryCode; final cn = address.countryName; @@ -493,12 +506,12 @@ class AvesEntry { } } - Future findAddressLine() async { + Future findAddressLine() async { if (!hasGps) return null; try { - final addresses = await GeocodingService.getAddress(latLng, geocoderLocale); - if (addresses != null && addresses.isNotEmpty) { + final addresses = await GeocodingService.getAddress(latLng!, geocoderLocale); + if (addresses.isNotEmpty) { final address = addresses.first; return address.addressLine; } @@ -549,12 +562,12 @@ class AvesEntry { if (isFlipped is bool) this.isFlipped = isFlipped; await metadataDb.saveEntries({this}); - await metadataDb.saveMetadata({catalogMetadata}); + if (catalogMetadata != null) await metadataDb.saveMetadata({catalogMetadata!}); metadataChangeNotifier.notifyListeners(); } - Future rotate({@required bool clockwise}) async { + Future rotate({required bool clockwise}) async { final newFields = await imageFileService.rotate(this, clockwise: clockwise); if (newFields.isEmpty) return false; @@ -579,7 +592,7 @@ class AvesEntry { } Future delete() { - Completer completer = Completer(); + final completer = Completer(); imageFileService.delete([this]).listen( (event) => completer.complete(event.success), onError: completer.completeError, @@ -593,7 +606,7 @@ class AvesEntry { } // when the entry image itself changed (e.g. after rotation) - Future _onImageChanged(int oldDateModifiedSecs, int oldRotationDegrees, bool oldIsFlipped) async { + Future _onImageChanged(int? oldDateModifiedSecs, int oldRotationDegrees, bool oldIsFlipped) async { if (oldDateModifiedSecs != dateModifiedSecs || oldRotationDegrees != rotationDegrees || oldIsFlipped != isFlipped) { await EntryCache.evict(uri, mimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped); imageChangeNotifier.notifyListeners(); @@ -626,15 +639,15 @@ class AvesEntry { // 1) title ascending // 2) extension ascending static int compareByName(AvesEntry a, AvesEntry b) { - final c = compareAsciiUpperCase(a.bestTitle, b.bestTitle); - return c != 0 ? c : compareAsciiUpperCase(a.extension, b.extension); + final c = compareAsciiUpperCase(a.bestTitle ?? '', b.bestTitle ?? ''); + return c != 0 ? c : compareAsciiUpperCase(a.extension ?? '', b.extension ?? ''); } // compare by: // 1) size descending // 2) name ascending static int compareBySize(AvesEntry a, AvesEntry b) { - final c = b.sizeBytes.compareTo(a.sizeBytes); + final c = (b.sizeBytes ?? 0).compareTo(a.sizeBytes ?? 0); return c != 0 ? c : compareByName(a, b); } diff --git a/lib/model/entry_cache.dart b/lib/model/entry_cache.dart index b535c0973..a70fd6d63 100644 --- a/lib/model/entry_cache.dart +++ b/lib/model/entry_cache.dart @@ -1,19 +1,29 @@ import 'dart:async'; -import 'dart:math'; import 'package:aves/image_providers/thumbnail_provider.dart'; import 'package:aves/image_providers/uri_image_provider.dart'; class EntryCache { + // ordered descending + static final thumbnailRequestExtents = []; + + static void markThumbnailExtent(double extent) { + if (!thumbnailRequestExtents.contains(extent)) { + thumbnailRequestExtents + ..add(extent) + ..sort((a, b) => b.compareTo(a)); + } + } + static Future evict( String uri, String mimeType, - int dateModifiedSecs, + int? dateModifiedSecs, int oldRotationDegrees, bool oldIsFlipped, ) async { // TODO TLAD provide pageId parameter for multi page items, if someday image editing features are added for them - int pageId; + int? pageId; // evict fullscreen image await UriImage( @@ -29,20 +39,18 @@ class EntryCache { uri: uri, mimeType: mimeType, pageId: pageId, - dateModifiedSecs: dateModifiedSecs, + dateModifiedSecs: dateModifiedSecs ?? 0, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, )).evict(); - // evict higher quality thumbnails (with powers of 2 from 32 to 1024 as specified extents) - final extents = List.generate(6, (index) => pow(2, index + 5).toDouble()); await Future.forEach( - extents, + thumbnailRequestExtents, (extent) => ThumbnailProvider(ThumbnailProviderKey( uri: uri, mimeType: mimeType, pageId: pageId, - dateModifiedSecs: dateModifiedSecs, + dateModifiedSecs: dateModifiedSecs ?? 0, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, extent: extent, diff --git a/lib/model/entry_images.dart b/lib/model/entry_images.dart index cc8c28374..d62dfabd7 100644 --- a/lib/model/entry_images.dart +++ b/lib/model/entry_images.dart @@ -5,7 +5,8 @@ import 'package:aves/image_providers/region_provider.dart'; import 'package:aves/image_providers/thumbnail_provider.dart'; import 'package:aves/image_providers/uri_image_provider.dart'; import 'package:aves/model/entry.dart'; -import 'package:flutter/foundation.dart'; +import 'package:aves/model/entry_cache.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; extension ExtraAvesEntry on AvesEntry { @@ -14,11 +15,7 @@ extension ExtraAvesEntry on AvesEntry { } ThumbnailProviderKey _getThumbnailProviderKey(double extent) { - // we standardize the thumbnail loading dimension by taking the nearest larger power of 2 - // so that there are less variants of the thumbnails to load and cache - // it increases the chance of cache hit when loading similarly sized columns (e.g. on orientation change) - final requestExtent = extent == 0 ? .0 : pow(2, (log(extent) / log(2)).ceil()).toDouble(); - + EntryCache.markThumbnailExtent(extent); return ThumbnailProviderKey( uri: uri, mimeType: mimeType, @@ -26,15 +23,15 @@ extension ExtraAvesEntry on AvesEntry { rotationDegrees: rotationDegrees, isFlipped: isFlipped, dateModifiedSecs: dateModifiedSecs ?? -1, - extent: requestExtent, + extent: extent, ); } - RegionProvider getRegion({@required int sampleSize, Rectangle region}) { + RegionProvider getRegion({required int sampleSize, Rectangle? region}) { return RegionProvider(_getRegionProviderKey(sampleSize, region)); } - RegionProviderKey _getRegionProviderKey(int sampleSize, Rectangle region) { + RegionProviderKey _getRegionProviderKey(int sampleSize, Rectangle? region) { return RegionProviderKey( uri: uri, mimeType: mimeType, @@ -56,12 +53,12 @@ extension ExtraAvesEntry on AvesEntry { expectedContentLength: sizeBytes, ); - bool _isReady(Object providerKey) => imageCache.statusForKey(providerKey).keepAlive; + bool _isReady(Object providerKey) => imageCache!.statusForKey(providerKey).keepAlive; - ImageProvider getBestThumbnail(double extent) { - final sizedThumbnailKey = _getThumbnailProviderKey(extent); - if (_isReady(sizedThumbnailKey)) return ThumbnailProvider(sizedThumbnailKey); + List get cachedThumbnails => EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).where(_isReady).map((key) => ThumbnailProvider(key)).toList(); - return getThumbnail(); + ThumbnailProvider get bestCachedThumbnail { + final sizedThumbnailKey = EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).firstWhereOrNull(_isReady); + return sizedThumbnailKey != null ? ThumbnailProvider(sizedThumbnailKey) : getThumbnail(); } } diff --git a/lib/model/favourites.dart b/lib/model/favourites.dart index 1ae6d54b9..524ad4be3 100644 --- a/lib/model/favourites.dart +++ b/lib/model/favourites.dart @@ -1,5 +1,6 @@ import 'package:aves/model/entry.dart'; import 'package:aves/services/services.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -18,7 +19,7 @@ class Favourites with ChangeNotifier { bool isFavourite(AvesEntry entry) => _rows.any((row) => row.contentId == entry.contentId); - FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId, path: entry.path); + FavouriteRow _entryToRow(AvesEntry entry) => FavouriteRow(contentId: entry.contentId!, path: entry.path!); Future add(Iterable entries) async { final newRows = entries.map(_entryToRow); @@ -40,7 +41,7 @@ class Favourites with ChangeNotifier { } Future moveEntry(int oldContentId, AvesEntry entry) async { - final oldRow = _rows.firstWhere((row) => row.contentId == oldContentId, orElse: () => null); + final oldRow = _rows.firstWhereOrNull((row) => row.contentId == oldContentId); if (oldRow == null) return; final newRow = _entryToRow(entry); @@ -66,13 +67,13 @@ class FavouriteRow { final String path; const FavouriteRow({ - this.contentId, - this.path, + required this.contentId, + required this.path, }); factory FavouriteRow.fromMap(Map map) { return FavouriteRow( - contentId: map['contentId'], + contentId: map['contentId'] ?? 0, path: map['path'] ?? '', ); } diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index edab0e9bc..7e8c708e5 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -14,7 +14,7 @@ class AlbumFilter extends CollectionFilter { static final Map _appColors = {}; final String album; - final String displayName; + final String? displayName; const AlbumFilter(this.album, this.displayName); @@ -41,10 +41,10 @@ class AlbumFilter extends CollectionFilter { String getTooltip(BuildContext context) => album; @override - Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) { + Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) { return IconUtils.getAlbumIcon( context: context, - album: album, + albumPath: album, size: size, embossed: embossed, ) ?? @@ -56,21 +56,20 @@ class AlbumFilter extends CollectionFilter { // do not use async/await and rely on `SynchronousFuture` // to prevent rebuilding of the `FutureBuilder` listening on this future if (androidFileUtils.getAlbumType(album) == AlbumType.app) { - if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]); + if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]!); - return PaletteGenerator.fromImageProvider( - AppIconImage( - packageName: androidFileUtils.getAlbumAppPackageName(album), - size: 24, - ), - ).then((palette) { - final color = palette.dominantColor?.color ?? super.color(context); - _appColors[album] = color; - return color; - }); - } else { - return super.color(context); + final packageName = androidFileUtils.getAlbumAppPackageName(album); + if (packageName != null) { + return PaletteGenerator.fromImageProvider( + AppIconImage(packageName: packageName, size: 24), + ).then((palette) async { + final color = palette.dominantColor?.color ?? (await super.color(context)); + _appColors[album] = color; + return color; + }); + } } + return super.color(context); } @override diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index fce3a889f..d0da56c81 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -24,7 +24,7 @@ abstract class CollectionFilter implements Comparable { TagFilter.type, ]; - static CollectionFilter fromJson(String jsonString) { + static CollectionFilter? fromJson(String jsonString) { final jsonMap = jsonDecode(jsonString); final type = jsonMap['type']; switch (type) { @@ -63,7 +63,7 @@ abstract class CollectionFilter implements Comparable { String getTooltip(BuildContext context) => getLabel(context); - Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}); + Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}); Future color(BuildContext context) => SynchronousFuture(stringToColor(getLabel(context))); @@ -84,7 +84,7 @@ abstract class CollectionFilter implements Comparable { class FilterGridItem { final T filter; - final AvesEntry entry; + final AvesEntry? entry; const FilterGridItem(this.filter, this.entry); diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index b92a0073b..4e78ac4d4 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -1,6 +1,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -10,8 +11,8 @@ class LocationFilter extends CollectionFilter { final LocationLevel level; String _location; - String _countryCode; - EntryFilter _test; + String? _countryCode; + late EntryFilter _test; LocationFilter(this.level, this._location) { final split = _location.split(locationSeparator); @@ -29,7 +30,7 @@ class LocationFilter extends CollectionFilter { LocationFilter.fromMap(Map json) : this( - LocationLevel.values.firstWhere((v) => v.toString() == json['level'], orElse: () => null), + LocationLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? LocationLevel.place, json['location'], ); @@ -42,7 +43,7 @@ class LocationFilter extends CollectionFilter { String get countryNameAndCode => '$_location$locationSeparator$_countryCode'; - String get countryCode => _countryCode; + String? get countryCode => _countryCode; @override EntryFilter get test => _test; @@ -90,8 +91,9 @@ class LocationFilter extends CollectionFilter { // U+1F1E6 🇦 REGIONAL INDICATOR SYMBOL LETTER A static const _countryCodeToFlagDiff = 0x1F1E6 - 0x0041; - static String countryCodeToFlag(String code) { - return code?.length == 2 ? String.fromCharCodes(code.codeUnits.map((letter) => letter += _countryCodeToFlagDiff)) : null; + static String? countryCodeToFlag(String? code) { + if (code == null || code.length != 2) return null; + return String.fromCharCodes(code.codeUnits.map((letter) => letter += _countryCodeToFlagDiff)); } } diff --git a/lib/model/filters/mime.dart b/lib/model/filters/mime.dart index 8ed351750..0d447a62e 100644 --- a/lib/model/filters/mime.dart +++ b/lib/model/filters/mime.dart @@ -10,9 +10,9 @@ class MimeFilter extends CollectionFilter { static const type = 'mime'; final String mime; - EntryFilter _test; - String _label; - IconData _icon; + late EntryFilter _test; + late String _label; + IconData? /*late*/ _icon; static final image = MimeFilter(MimeTypes.anyImage); static final video = MimeFilter(MimeTypes.anyVideo); diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index cab4778e7..c9ffc60a2 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -12,7 +12,7 @@ class QueryFilter extends CollectionFilter { final String query; final bool colorful; - EntryFilter _test; + late EntryFilter _test; QueryFilter(this.query, {this.colorful = true}) { var upQuery = query.toUpperCase(); @@ -26,7 +26,7 @@ class QueryFilter extends CollectionFilter { // allow untrimmed queries wrapped with `"..."` final matches = exactRegex.allMatches(upQuery); if (matches.length == 1) { - upQuery = matches.first.group(1); + upQuery = matches.first.group(1)!; } _test = not ? (entry) => !entry.search(upQuery) : (entry) => entry.search(upQuery); diff --git a/lib/model/filters/tag.dart b/lib/model/filters/tag.dart index 3b39b82ac..c64fa04d2 100644 --- a/lib/model/filters/tag.dart +++ b/lib/model/filters/tag.dart @@ -8,7 +8,7 @@ class TagFilter extends CollectionFilter { static const type = 'tag'; final String tag; - EntryFilter _test; + late EntryFilter _test; TagFilter(this.tag) { if (tag.isEmpty) { @@ -42,7 +42,7 @@ class TagFilter extends CollectionFilter { String getLabel(BuildContext context) => tag.isEmpty ? context.l10n.filterTagEmptyLabel : tag; @override - Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagOff : AIcons.tag, size: size) : null; + Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagOff : AIcons.tag, size: size) : null; @override String get category => type; diff --git a/lib/model/filters/type.dart b/lib/model/filters/type.dart index 9087f1879..a808971c4 100644 --- a/lib/model/filters/type.dart +++ b/lib/model/filters/type.dart @@ -14,8 +14,8 @@ class TypeFilter extends CollectionFilter { static const _sphericalVideo = 'spherical_video'; // subset of videos final String itemType; - EntryFilter _test; - IconData _icon; + late EntryFilter _test; + IconData? /*late*/ _icon; static final animated = TypeFilter._private(_animated); static final geotiff = TypeFilter._private(_geotiff); diff --git a/lib/model/highlight.dart b/lib/model/highlight.dart index 589ada79e..642abe8ac 100644 --- a/lib/model/highlight.dart +++ b/lib/model/highlight.dart @@ -1,7 +1,29 @@ +import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; class HighlightInfo extends ChangeNotifier { - Object _item; + final EventBus eventBus = EventBus(); + + void trackItem( + T? item, { + TrackPredicate? predicate, + Alignment? alignment, + bool? animate, + Object? highlightItem, + }) { + if (item != null) { + eventBus.fire(TrackEvent( + item, + predicate ?? (_) => true, + alignment ?? Alignment.center, + animate ?? true, + highlightItem, + )); + } + } + + Object? _item; void set(Object item) { if (_item == item) return; @@ -9,7 +31,7 @@ class HighlightInfo extends ChangeNotifier { notifyListeners(); } - Object clear() { + Object? clear() { if (_item == null) return null; final item = _item; _item = null; @@ -22,3 +44,24 @@ class HighlightInfo extends ChangeNotifier { @override String toString() => '$runtimeType#${shortHash(this)}{item=$_item}'; } + +@immutable +class TrackEvent { + final T item; + final TrackPredicate predicate; + final Alignment alignment; + final bool animate; + final Object? highlightItem; + + const TrackEvent( + this.item, + this.predicate, + this.alignment, + this.animate, + this.highlightItem, + ); +} + +// `itemVisibility`: percent of the item tracked already visible in viewport +// return whether to proceed with tracking +typedef TrackPredicate = bool Function(double itemVisibility); diff --git a/lib/model/metadata.dart b/lib/model/metadata.dart index 2d8157faf..2bb707e00 100644 --- a/lib/model/metadata.dart +++ b/lib/model/metadata.dart @@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; class DateMetadata { - final int contentId, dateMillis; + final int? contentId, dateMillis; DateMetadata({ this.contentId, @@ -28,13 +28,13 @@ class DateMetadata { } class CatalogMetadata { - final int contentId, dateMillis; + final int? contentId, dateMillis; final bool isAnimated, isGeotiff, is360, isMultiPage; bool isFlipped; - int rotationDegrees; - final String mimeType, xmpSubjects, xmpTitleDescription; - double latitude, longitude; - Address address; + int? rotationDegrees; + final String? mimeType, xmpSubjects, xmpTitleDescription; + double? latitude, longitude; + Address? address; static const double _precisionErrorTolerance = 1e-9; static const _isAnimatedMask = 1 << 0; @@ -55,23 +55,28 @@ class CatalogMetadata { this.rotationDegrees, this.xmpSubjects, this.xmpTitleDescription, - double latitude, - double longitude, + double? latitude, + double? longitude, }) { - // Geocoder throws an `IllegalArgumentException` when a coordinate has a funky values like `1.7056881853375E7` + // Geocoder throws an `IllegalArgumentException` when a coordinate has a funky value like `1.7056881853375E7` // We also exclude zero coordinates, taking into account precision errors (e.g. {5.952380952380953e-11,-2.7777777777777777e-10}), // but Flutter's `precisionErrorTolerance` (1e-10) is slightly too lenient for this case. if (latitude != null && longitude != null && (latitude.abs() > _precisionErrorTolerance || longitude.abs() > _precisionErrorTolerance)) { - this.latitude = latitude < -90.0 || latitude > 90.0 ? null : latitude; - this.longitude = longitude < -180.0 || longitude > 180.0 ? null : longitude; + // funny case: some files have latitude and longitude reverse + // (e.g. a Japanese location at lat~=133 and long~=34, which is a valid longitude but an invalid latitude) + // so we should check and assign both coordinates at once + if (latitude >= -90.0 && latitude <= 90.0 && longitude >= -180.0 && longitude <= 180.0) { + this.latitude = latitude; + this.longitude = longitude; + } } } CatalogMetadata copyWith({ - int contentId, - String mimeType, - bool isMultiPage, - int rotationDegrees, + int? contentId, + String? mimeType, + bool? isMultiPage, + int? rotationDegrees, }) { return CatalogMetadata( contentId: contentId ?? this.contentId, @@ -127,16 +132,16 @@ class CatalogMetadata { } class OverlayMetadata { - final String aperture, exposureTime, focalLength, iso; + final String? aperture, exposureTime, focalLength, iso; static final apertureFormat = NumberFormat('0.0', 'en_US'); static final focalLengthFormat = NumberFormat('0.#', 'en_US'); OverlayMetadata({ - double aperture, - String exposureTime, - double focalLength, - int iso, + double? aperture, + String? exposureTime, + double? focalLength, + int? iso, }) : aperture = aperture != null ? 'ƒ/${apertureFormat.format(aperture)}' : null, exposureTime = exposureTime, focalLength = focalLength != null ? '${focalLengthFormat.format(focalLength)} mm' : null, @@ -144,10 +149,10 @@ class OverlayMetadata { factory OverlayMetadata.fromMap(Map map) { return OverlayMetadata( - aperture: map['aperture'] as double, - exposureTime: map['exposureTime'] as String, - focalLength: map['focalLength'] as double, - iso: map['iso'] as int, + aperture: map['aperture'] as double?, + exposureTime: map['exposureTime'] as String?, + focalLength: map['focalLength'] as double?, + iso: map['iso'] as int?, ); } @@ -159,10 +164,10 @@ class OverlayMetadata { @immutable class AddressDetails { - final int contentId; - final String countryCode, countryName, adminArea, locality; + final int? contentId; + final String? countryCode, countryName, adminArea, locality; - String get place => locality != null && locality.isNotEmpty ? locality : adminArea; + String? get place => locality != null && locality!.isNotEmpty ? locality : adminArea; const AddressDetails({ this.contentId, @@ -173,7 +178,7 @@ class AddressDetails { }); AddressDetails copyWith({ - int contentId, + int? contentId, }) { return AddressDetails( contentId: contentId ?? this.contentId, @@ -186,11 +191,11 @@ class AddressDetails { factory AddressDetails.fromMap(Map map) { return AddressDetails( - contentId: map['contentId'], - countryCode: map['countryCode'] ?? '', - countryName: map['countryName'] ?? '', - adminArea: map['adminArea'] ?? '', - locality: map['locality'] ?? '', + contentId: map['contentId'] as int?, + countryCode: map['countryCode'] as String?, + countryName: map['countryName'] as String?, + adminArea: map['adminArea'] as String?, + locality: map['locality'] as String?, ); } diff --git a/lib/model/metadata_db.dart b/lib/model/metadata_db.dart index 1e1474325..2afdbcedf 100644 --- a/lib/model/metadata_db.dart +++ b/lib/model/metadata_db.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:aves/model/covers.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db_upgrade.dart'; import 'package:aves/services/services.dart'; @@ -16,7 +17,7 @@ abstract class MetadataDb { Future reset(); - Future removeIds(Set contentIds, {@required bool metadataOnly}); + Future removeIds(Set contentIds, {required bool metadataOnly}); // entries @@ -40,9 +41,9 @@ abstract class MetadataDb { Future> loadMetadataEntries(); - Future saveMetadata(Iterable metadataEntries); + Future saveMetadata(Set metadataEntries); - Future updateMetadataId(int oldId, CatalogMetadata metadata); + Future updateMetadataId(int oldId, CatalogMetadata? metadata); // address @@ -50,9 +51,9 @@ abstract class MetadataDb { Future> loadAddresses(); - Future saveAddresses(Iterable addresses); + Future saveAddresses(Set addresses); - Future updateAddressId(int oldId, AddressDetails address); + Future updateAddressId(int oldId, AddressDetails? address); // favourites @@ -76,11 +77,11 @@ abstract class MetadataDb { Future updateCoverEntryId(int oldId, CoverRow row); - Future removeCovers(Iterable rows); + Future removeCovers(Set filters); } class SqfliteMetadataDb implements MetadataDb { - Future _database; + late Future _database; Future get path async => pContext.join(await getDatabasesPath(), 'metadata.db'); @@ -150,8 +151,8 @@ class SqfliteMetadataDb implements MetadataDb { @override Future dbFileSize() async { - final file = File((await path)); - return await file.exists() ? file.length() : 0; + final file = File(await path); + return await file.exists() ? await file.length() : 0; } @override @@ -163,8 +164,8 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future removeIds(Set contentIds, {@required bool metadataOnly}) async { - if (contentIds == null || contentIds.isEmpty) return; + Future removeIds(Set contentIds, {required bool metadataOnly}) async { + if (contentIds.isEmpty) return; final stopwatch = Stopwatch()..start(); final db = await _database; @@ -207,7 +208,7 @@ class SqfliteMetadataDb implements MetadataDb { @override Future saveEntries(Iterable entries) async { - if (entries == null || entries.isEmpty) return; + if (entries.isEmpty) return; final stopwatch = Stopwatch()..start(); final db = await _database; final batch = db.batch(); @@ -226,7 +227,6 @@ class SqfliteMetadataDb implements MetadataDb { } void _batchInsertEntry(Batch batch, AvesEntry entry) { - if (entry == null) return; batch.insert( entryTable, entry.toMap(), @@ -273,13 +273,13 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future saveMetadata(Iterable metadataEntries) async { - if (metadataEntries == null || metadataEntries.isEmpty) return; + Future saveMetadata(Set metadataEntries) async { + if (metadataEntries.isEmpty) return; final stopwatch = Stopwatch()..start(); try { final db = await _database; final batch = db.batch(); - metadataEntries.where((metadata) => metadata != null).forEach((metadata) => _batchInsertMetadata(batch, metadata)); + metadataEntries.forEach((metadata) => _batchInsertMetadata(batch, metadata)); await batch.commit(noResult: true); debugPrint('$runtimeType saveMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries'); } catch (error, stack) { @@ -288,7 +288,7 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future updateMetadataId(int oldId, CatalogMetadata metadata) async { + Future updateMetadataId(int oldId, CatalogMetadata? metadata) async { final db = await _database; final batch = db.batch(); batch.delete(dateTakenTable, where: 'contentId = ?', whereArgs: [oldId]); @@ -297,7 +297,7 @@ class SqfliteMetadataDb implements MetadataDb { await batch.commit(noResult: true); } - void _batchInsertMetadata(Batch batch, CatalogMetadata metadata) { + void _batchInsertMetadata(Batch batch, CatalogMetadata? metadata) { if (metadata == null) return; if (metadata.dateMillis != 0) { batch.insert( @@ -333,18 +333,18 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future saveAddresses(Iterable addresses) async { - if (addresses == null || addresses.isEmpty) return; + Future saveAddresses(Set addresses) async { + if (addresses.isEmpty) return; final stopwatch = Stopwatch()..start(); final db = await _database; final batch = db.batch(); - addresses.where((address) => address != null).forEach((address) => _batchInsertAddress(batch, address)); + addresses.forEach((address) => _batchInsertAddress(batch, address)); await batch.commit(noResult: true); debugPrint('$runtimeType saveAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries'); } @override - Future updateAddressId(int oldId, AddressDetails address) async { + Future updateAddressId(int oldId, AddressDetails? address) async { final db = await _database; final batch = db.batch(); batch.delete(addressTable, where: 'contentId = ?', whereArgs: [oldId]); @@ -352,7 +352,7 @@ class SqfliteMetadataDb implements MetadataDb { await batch.commit(noResult: true); } - void _batchInsertAddress(Batch batch, AddressDetails address) { + void _batchInsertAddress(Batch batch, AddressDetails? address) { if (address == null) return; batch.insert( addressTable, @@ -380,10 +380,10 @@ class SqfliteMetadataDb implements MetadataDb { @override Future addFavourites(Iterable rows) async { - if (rows == null || rows.isEmpty) return; + if (rows.isEmpty) return; final db = await _database; final batch = db.batch(); - rows.where((row) => row != null).forEach((row) => _batchInsertFavourite(batch, row)); + rows.forEach((row) => _batchInsertFavourite(batch, row)); await batch.commit(noResult: true); } @@ -397,7 +397,6 @@ class SqfliteMetadataDb implements MetadataDb { } void _batchInsertFavourite(Batch batch, FavouriteRow row) { - if (row == null) return; batch.insert( favouriteTable, row.toMap(), @@ -407,8 +406,8 @@ class SqfliteMetadataDb implements MetadataDb { @override Future removeFavourites(Iterable rows) async { - if (rows == null || rows.isEmpty) return; - final ids = rows.where((row) => row != null).map((row) => row.contentId); + if (rows.isEmpty) return; + final ids = rows.map((row) => row.contentId); if (ids.isEmpty) return; final db = await _database; @@ -431,16 +430,16 @@ class SqfliteMetadataDb implements MetadataDb { Future> loadCovers() async { final db = await _database; final maps = await db.query(coverTable); - final rows = maps.map((map) => CoverRow.fromMap(map)).toSet(); + final rows = maps.map(CoverRow.fromMap).where((v) => v != null).cast().toSet(); return rows; } @override Future addCovers(Iterable rows) async { - if (rows == null || rows.isEmpty) return; + if (rows.isEmpty) return; final db = await _database; final batch = db.batch(); - rows.where((row) => row != null).forEach((row) => _batchInsertCover(batch, row)); + rows.forEach((row) => _batchInsertCover(batch, row)); await batch.commit(noResult: true); } @@ -454,7 +453,6 @@ class SqfliteMetadataDb implements MetadataDb { } void _batchInsertCover(Batch batch, CoverRow row) { - if (row == null) return; batch.insert( coverTable, row.toMap(), @@ -463,9 +461,7 @@ class SqfliteMetadataDb implements MetadataDb { } @override - Future removeCovers(Iterable rows) async { - if (rows == null || rows.isEmpty) return; - final filters = rows.where((row) => row != null).map((row) => row.filter); + Future removeCovers(Set filters) async { if (filters.isEmpty) return; final db = await _database; diff --git a/lib/model/multipage.dart b/lib/model/multipage.dart index bc92ef4d7..c2478077d 100644 --- a/lib/model/multipage.dart +++ b/lib/model/multipage.dart @@ -1,6 +1,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/services.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; class MultiPageInfo { @@ -11,8 +12,8 @@ class MultiPageInfo { int get pageCount => _pages.length; MultiPageInfo({ - @required this.mainEntry, - List pages, + required this.mainEntry, + required List pages, }) : _pages = pages { if (_pages.isNotEmpty) { _pages.sort(); @@ -31,15 +32,15 @@ class MultiPageInfo { ); } - SinglePageInfo get defaultPage => _pages.firstWhere((page) => page.isDefault, orElse: () => null); + SinglePageInfo? get defaultPage => _pages.firstWhereOrNull((page) => page.isDefault); - SinglePageInfo getById(int pageId) => _pages.firstWhere((page) => page.pageId == pageId, orElse: () => null); + SinglePageInfo? getById(int? pageId) => _pages.firstWhereOrNull((page) => page.pageId == pageId); - SinglePageInfo getByIndex(int pageIndex) => _pages.firstWhere((page) => page.index == pageIndex, orElse: () => null); + SinglePageInfo? getByIndex(int? pageIndex) => _pages.firstWhereOrNull((page) => page.index == pageIndex); - AvesEntry getPageEntryByIndex(int pageIndex) => _getPageEntry(getByIndex(pageIndex)); + AvesEntry getPageEntryByIndex(int? pageIndex) => _getPageEntry(getByIndex(pageIndex)); - AvesEntry _getPageEntry(SinglePageInfo pageInfo) { + AvesEntry _getPageEntry(SinglePageInfo? pageInfo) { if (pageInfo != null) { return _pageEntries.putIfAbsent(pageInfo, () => _createPageEntry(pageInfo)); } else { @@ -52,20 +53,20 @@ class MultiPageInfo { List get exportEntries => _pages.map((pageInfo) => _createPageEntry(pageInfo, eraseDefaultPageId: false)).toList(); Future extractMotionPhotoVideo() async { - final videoPage = _pages.firstWhere((page) => page.isVideo, orElse: () => null); + final videoPage = _pages.firstWhereOrNull((page) => page.isVideo); if (videoPage != null && videoPage.uri == null) { final fields = await embeddedDataService.extractMotionPhotoVideo(mainEntry); - if (fields != null) { + if (fields.containsKey('uri')) { final pageIndex = _pages.indexOf(videoPage); _pages.removeAt(pageIndex); _pages.insert( pageIndex, videoPage.copyWith( - uri: fields['uri'] as String, + uri: fields['uri'] as String?, // the initial fake page may contain inaccurate values for the following fields // so we override them with values from the extracted standalone video - rotationDegrees: fields['sourceRotationDegrees'] as int, - durationMillis: fields['durationMillis'] as int, + rotationDegrees: fields['sourceRotationDegrees'] as int?, + durationMillis: fields['durationMillis'] as int?, )); _pageEntries.remove(videoPage); } @@ -83,9 +84,9 @@ class MultiPageInfo { path: mainEntry.path, contentId: mainEntry.contentId, pageId: pageId, - sourceMimeType: pageInfo.mimeType ?? mainEntry.sourceMimeType, - width: pageInfo.width ?? mainEntry.width, - height: pageInfo.height ?? mainEntry.height, + sourceMimeType: pageInfo.mimeType, + width: pageInfo.width, + height: pageInfo.height, sourceRotationDegrees: pageInfo.rotationDegrees ?? mainEntry.sourceRotationDegrees, sizeBytes: mainEntry.sizeBytes, sourceTitle: mainEntry.sourceTitle, @@ -108,26 +109,28 @@ class MultiPageInfo { class SinglePageInfo implements Comparable { final int index, pageId; final bool isDefault; - final String uri, mimeType; - final int width, height, rotationDegrees, durationMillis; + final String? uri; + final String mimeType; + final int width, height; + final int? rotationDegrees, durationMillis; const SinglePageInfo({ - this.index, - this.pageId, - this.isDefault, + required this.index, + required this.pageId, + required this.isDefault, this.uri, - this.mimeType, - this.width, - this.height, + required this.mimeType, + required this.width, + required this.height, this.rotationDegrees, this.durationMillis, }); SinglePageInfo copyWith({ - bool isDefault, - String uri, - int rotationDegrees, - int durationMillis, + bool? isDefault, + String? uri, + int? rotationDegrees, + int? durationMillis, }) { return SinglePageInfo( index: index, @@ -147,12 +150,12 @@ class SinglePageInfo implements Comparable { return SinglePageInfo( index: index, pageId: index, - isDefault: map['isDefault'] as bool ?? false, + isDefault: map['isDefault'] as bool? ?? false, mimeType: map['mimeType'] as String, - width: map['width'] as int ?? 0, - height: map['height'] as int ?? 0, - rotationDegrees: map['rotationDegrees'] as int, - durationMillis: map['durationMillis'] as int, + width: map['width'] as int? ?? 0, + height: map['height'] as int? ?? 0, + rotationDegrees: map['rotationDegrees'] as int?, + durationMillis: map['durationMillis'] as int?, ); } diff --git a/lib/model/panorama.dart b/lib/model/panorama.dart index 99d1ff318..df67ae060 100644 --- a/lib/model/panorama.dart +++ b/lib/model/panorama.dart @@ -2,9 +2,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; class PanoramaInfo { - final Rect croppedAreaRect; - final Size fullPanoSize; - final String projectionType; + final Rect? croppedAreaRect; + final Size? fullPanoSize; + final String? projectionType; PanoramaInfo({ this.croppedAreaRect, @@ -13,13 +13,13 @@ class PanoramaInfo { }); factory PanoramaInfo.fromMap(Map map) { - var cLeft = map['croppedAreaLeft'] as int; - var cTop = map['croppedAreaTop'] as int; - final cWidth = map['croppedAreaWidth'] as int; - final cHeight = map['croppedAreaHeight'] as int; - var fWidth = map['fullPanoWidth'] as int; - var fHeight = map['fullPanoHeight'] as int; - final projectionType = map['projectionType'] as String; + var cLeft = map['croppedAreaLeft'] as int?; + var cTop = map['croppedAreaTop'] as int?; + final cWidth = map['croppedAreaWidth'] as int?; + final cHeight = map['croppedAreaHeight'] as int?; + var fWidth = map['fullPanoWidth'] as int?; + var fHeight = map['fullPanoHeight'] as int?; + final projectionType = map['projectionType'] as String?; // handle missing `fullPanoHeight` (e.g. Samsung camera app panorama mode) if (fHeight == null && cWidth != null && cHeight != null) { @@ -31,12 +31,12 @@ class PanoramaInfo { cLeft = 0; } - Rect croppedAreaRect; + Rect? croppedAreaRect; if (cLeft != null && cTop != null && cWidth != null && cHeight != null) { croppedAreaRect = Rect.fromLTWH(cLeft.toDouble(), cTop.toDouble(), cWidth.toDouble(), cHeight.toDouble()); } - Size fullPanoSize; + Size? fullPanoSize; if (fWidth != null && fHeight != null) { fullPanoSize = Size(fWidth.toDouble(), fHeight.toDouble()); } diff --git a/lib/model/settings/entry_background.dart b/lib/model/settings/entry_background.dart index 14f83f071..c9935b6d1 100644 --- a/lib/model/settings/entry_background.dart +++ b/lib/model/settings/entry_background.dart @@ -15,12 +15,11 @@ extension ExtraEntryBackground on EntryBackground { Color get color { switch (this) { - case EntryBackground.black: - return Colors.black; case EntryBackground.white: return Colors.white; + case EntryBackground.black: default: - return null; + return Colors.black; } } } diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index e837d2e64..cb68e2ff6 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -1,6 +1,7 @@ import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/screen_on.dart'; +import 'package:collection/collection.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; @@ -14,7 +15,7 @@ import 'enums.dart'; final Settings settings = Settings._private(); class Settings extends ChangeNotifier { - static SharedPreferences _prefs; + static SharedPreferences? /*late final*/ _prefs; Settings._private(); @@ -93,7 +94,7 @@ class Settings extends ChangeNotifier { } Future reset() { - return _prefs.clear(); + return _prefs!.clear(); } // app @@ -111,7 +112,7 @@ class Settings extends ChangeNotifier { static const localeSeparator = '-'; - Locale get locale { + Locale? get locale { // exceptionally allow getting locale before settings are initialized final tag = _prefs?.getString(localeKey); if (tag != null) { @@ -125,11 +126,11 @@ class Settings extends ChangeNotifier { return null; } - set locale(Locale newValue) { - String tag; + set locale(Locale? newValue) { + String? tag; if (newValue != null) { tag = [ - newValue.languageCode ?? '', + newValue.languageCode, newValue.scriptCode ?? '', newValue.countryCode ?? '', ].join(localeSeparator); @@ -152,11 +153,11 @@ class Settings extends ChangeNotifier { set homePage(HomePageSetting newValue) => setAndNotify(homePageKey, newValue.toString()); - String get catalogTimeZone => _prefs.getString(catalogTimeZoneKey) ?? ''; + String get catalogTimeZone => _prefs!.getString(catalogTimeZoneKey) ?? ''; set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue); - double getTileExtent(String routeName) => _prefs.getDouble(tileExtentPrefixKey + routeName) ?? 0; + double getTileExtent(String routeName) => _prefs!.getDouble(tileExtentPrefixKey + routeName) ?? 0; // do not notify, as tile extents are only used internally by `TileExtentController` // and should not trigger rebuilding by change notification @@ -202,11 +203,11 @@ class Settings extends ChangeNotifier { set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString()); - Set get pinnedFilters => (_prefs.getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).toSet(); + Set get pinnedFilters => (_prefs!.getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).where((v) => v != null).cast().toSet(); set pinnedFilters(Set newValue) => setAndNotify(pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList()); - Set get hiddenFilters => (_prefs.getStringList(hiddenFiltersKey) ?? []).map(CollectionFilter.fromJson).toSet(); + Set get hiddenFilters => (_prefs!.getStringList(hiddenFiltersKey) ?? []).map(CollectionFilter.fromJson).where((v) => v != null).cast().toSet(); set hiddenFilters(Set newValue) => setAndNotify(hiddenFiltersKey, newValue.map((filter) => filter.toJson()).toList()); @@ -248,7 +249,7 @@ class Settings extends ChangeNotifier { set infoMapStyle(EntryMapStyle newValue) => setAndNotify(infoMapStyleKey, newValue.toString()); - double get infoMapZoom => _prefs.getDouble(infoMapZoomKey) ?? 12; + double get infoMapZoom => _prefs!.getDouble(infoMapZoomKey) ?? 12; set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue); @@ -272,23 +273,23 @@ class Settings extends ChangeNotifier { set saveSearchHistory(bool newValue) => setAndNotify(saveSearchHistoryKey, newValue); - List get searchHistory => (_prefs.getStringList(searchHistoryKey) ?? []).map(CollectionFilter.fromJson).toList(); + List get searchHistory => (_prefs!.getStringList(searchHistoryKey) ?? []).map(CollectionFilter.fromJson).where((v) => v != null).cast().toList(); set searchHistory(List newValue) => setAndNotify(searchHistoryKey, newValue.map((filter) => filter.toJson()).toList()); // version - DateTime get lastVersionCheckDate => DateTime.fromMillisecondsSinceEpoch(_prefs.getInt(lastVersionCheckDateKey) ?? 0); + DateTime get lastVersionCheckDate => DateTime.fromMillisecondsSinceEpoch(_prefs!.getInt(lastVersionCheckDateKey) ?? 0); set lastVersionCheckDate(DateTime newValue) => setAndNotify(lastVersionCheckDateKey, newValue.millisecondsSinceEpoch); // convenience methods // ignore: avoid_positional_boolean_parameters - bool getBoolOrDefault(String key, bool defaultValue) => _prefs.getKeys().contains(key) ? _prefs.getBool(key) : defaultValue; + bool getBoolOrDefault(String key, bool defaultValue) => _prefs!.getBool(key) ?? defaultValue; T getEnumOrDefault(String key, T defaultValue, Iterable values) { - final valueString = _prefs.getString(key); + final valueString = _prefs!.getString(key); for (final v in values) { if (v.toString() == valueString) { return v; @@ -298,28 +299,28 @@ class Settings extends ChangeNotifier { } List getEnumListOrDefault(String key, List defaultValue, Iterable values) { - return _prefs.getStringList(key)?.map((s) => values.firstWhere((v) => v.toString() == s, orElse: () => null))?.where((v) => v != null)?.toList() ?? defaultValue; + return _prefs!.getStringList(key)?.map((s) => values.firstWhereOrNull((v) => v.toString() == s)).where((v) => v != null).cast().toList() ?? defaultValue; } void setAndNotify(String key, dynamic newValue, {bool notify = true}) { - var oldValue = _prefs.get(key); + var oldValue = _prefs!.get(key); if (newValue == null) { - _prefs.remove(key); + _prefs!.remove(key); } else if (newValue is String) { - oldValue = _prefs.getString(key); - _prefs.setString(key, newValue); + oldValue = _prefs!.getString(key); + _prefs!.setString(key, newValue); } else if (newValue is List) { - oldValue = _prefs.getStringList(key); - _prefs.setStringList(key, newValue); + oldValue = _prefs!.getStringList(key); + _prefs!.setStringList(key, newValue); } else if (newValue is int) { - oldValue = _prefs.getInt(key); - _prefs.setInt(key, newValue); + oldValue = _prefs!.getInt(key); + _prefs!.setInt(key, newValue); } else if (newValue is double) { - oldValue = _prefs.getDouble(key); - _prefs.setDouble(key, newValue); + oldValue = _prefs!.getDouble(key); + _prefs!.setDouble(key, newValue); } else if (newValue is bool) { - oldValue = _prefs.getBool(key); - _prefs.setBool(key, newValue); + oldValue = _prefs!.getBool(key); + _prefs!.setBool(key, newValue); } if (oldValue != newValue && notify) { notifyListeners(); diff --git a/lib/model/settings/video_loop_mode.dart b/lib/model/settings/video_loop_mode.dart index ec114540c..352b1d911 100644 --- a/lib/model/settings/video_loop_mode.dart +++ b/lib/model/settings/video_loop_mode.dart @@ -25,11 +25,10 @@ extension ExtraVideoLoopMode on VideoLoopMode { case VideoLoopMode.never: return false; case VideoLoopMode.shortOnly: - if (entry.durationMillis == null) return false; - return entry.durationMillis < shortVideoThreshold.inMilliseconds; + final durationMillis = entry.durationMillis; + return durationMillis != null ? durationMillis < shortVideoThreshold.inMilliseconds : false; case VideoLoopMode.always: return true; } - return false; } } diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index 72e2930e5..359bef7be 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.dart @@ -9,7 +9,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; mixin AlbumMixin on SourceBase { - final Set _directories = {}; + final Set _directories = {}; List get rawAlbums => List.unmodifiable(_directories); @@ -25,7 +25,7 @@ mixin AlbumMixin on SourceBase { void _notifyAlbumChange() => eventBus.fire(AlbumsChangedEvent()); - String getAlbumDisplayName(BuildContext context, String dirPath) { + String getAlbumDisplayName(BuildContext? context, String dirPath) { assert(!dirPath.endsWith(pContext.separator)); if (context != null) { @@ -41,15 +41,15 @@ mixin AlbumMixin on SourceBase { final relativeDir = dir.relativeDir; if (relativeDir.isEmpty) { - final volume = androidFileUtils.getStorageVolume(dirPath); + final volume = androidFileUtils.getStorageVolume(dirPath)!; return volume.getDescription(context); } - String unique(String dirPath, Set others) { + String unique(String dirPath, Set others) { final parts = pContext.split(dirPath); for (var i = parts.length - 1; i > 0; i--) { final testName = pContext.joinAll(['', ...parts.skip(i)]); - if (others.every((item) => !item.endsWith(testName))) return testName; + if (others.every((item) => !item!.endsWith(testName))) return testName; } return dirPath; } @@ -61,10 +61,10 @@ mixin AlbumMixin on SourceBase { } final volumePath = dir.volumePath; - String trimVolumePath(String path) => path.substring(dir.volumePath.length); - final otherAlbumsOnVolume = otherAlbumsOnDevice.where((path) => path.startsWith(volumePath)).map(trimVolumePath).toSet(); + String trimVolumePath(String? path) => path!.substring(dir.volumePath.length); + final otherAlbumsOnVolume = otherAlbumsOnDevice.where((path) => path!.startsWith(volumePath)).map(trimVolumePath).toSet(); final uniqueNameInVolume = unique(trimVolumePath(dirPath), otherAlbumsOnVolume); - final volume = androidFileUtils.getStorageVolume(dirPath); + final volume = androidFileUtils.getStorageVolume(dirPath)!; if (volume.isPrimary) { return uniqueNameInVolume; } else { @@ -72,7 +72,7 @@ mixin AlbumMixin on SourceBase { } } - Map getAlbumEntries() { + Map getAlbumEntries() { final entries = sortedEntriesByDate; final regularAlbums = [], appAlbums = [], specialAlbums = []; for (final album in rawAlbums) { @@ -90,7 +90,7 @@ mixin AlbumMixin on SourceBase { } return Map.fromEntries([...specialAlbums, ...appAlbums, ...regularAlbums].map((album) => MapEntry( album, - entries.firstWhere((entry) => entry.directory == album, orElse: () => null), + entries.firstWhereOrNull((entry) => entry.directory == album), ))); } @@ -100,14 +100,14 @@ mixin AlbumMixin on SourceBase { cleanEmptyAlbums(); } - void addDirectories(Set albums) { + void addDirectories(Set albums) { if (!_directories.containsAll(albums)) { _directories.addAll(albums); _notifyAlbumChange(); } } - void cleanEmptyAlbums([Set albums]) { + void cleanEmptyAlbums([Set? albums]) { final emptyAlbums = (albums ?? _directories).where(_isEmptyAlbum).toSet(); if (emptyAlbums.isNotEmpty) { _directories.removeAll(emptyAlbums); @@ -120,20 +120,20 @@ mixin AlbumMixin on SourceBase { } } - bool _isEmptyAlbum(String album) => !visibleEntries.any((entry) => entry.directory == album); + bool _isEmptyAlbum(String? album) => !visibleEntries.any((entry) => entry.directory == album); // filter summary // by directory final Map _filterEntryCountMap = {}; - final Map _filterRecentEntryMap = {}; + final Map _filterRecentEntryMap = {}; - void invalidateAlbumFilterSummary({Set entries, Set directories}) { + void invalidateAlbumFilterSummary({Set? entries, Set? directories}) { if (entries == null && directories == null) { _filterEntryCountMap.clear(); _filterRecentEntryMap.clear(); } else { - directories ??= entries.map((entry) => entry.directory).toSet(); + directories ??= entries!.map((entry) => entry.directory).toSet(); directories.forEach(_filterEntryCountMap.remove); directories.forEach(_filterRecentEntryMap.remove); } @@ -144,15 +144,15 @@ mixin AlbumMixin on SourceBase { return _filterEntryCountMap.putIfAbsent(filter.album, () => visibleEntries.where(filter.test).length); } - AvesEntry albumRecentEntry(AlbumFilter filter) { - return _filterRecentEntryMap.putIfAbsent(filter.album, () => sortedEntriesByDate.firstWhere(filter.test, orElse: () => null)); + AvesEntry? albumRecentEntry(AlbumFilter filter) { + return _filterRecentEntryMap.putIfAbsent(filter.album, () => sortedEntriesByDate.firstWhereOrNull(filter.test)); } } class AlbumsChangedEvent {} class AlbumSummaryInvalidatedEvent { - final Set directories; + final Set? directories; const AlbumSummaryInvalidatedEvent(this.directories); } diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index 9531ed566..f3d23bbc2 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'dart:collection'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/settings/settings.dart'; @@ -22,22 +24,22 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { EntryGroupFactor groupFactor; EntrySortFactor sortFactor; final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortGroupChangeNotifier = AChangeNotifier(); - int id; + final List _subscriptions = []; + int? id; bool listenToSource; - List _filteredSortedEntries; - List _subscriptions = []; + List _filteredSortedEntries = []; Map> sections = Map.unmodifiable({}); CollectionLens({ - @required this.source, - Iterable filters, - EntryGroupFactor groupFactor, - EntrySortFactor sortFactor, + required this.source, + Iterable? filters, + EntryGroupFactor? groupFactor, + EntrySortFactor? sortFactor, this.id, this.listenToSource = true, - }) : filters = {if (filters != null) ...filters.where((f) => f != null)}, + }) : filters = (filters ?? {}).where((f) => f != null).cast().toSet(), groupFactor = groupFactor ?? settings.collectionGroupFactor, sortFactor = sortFactor ?? settings.collectionSortFactor { id ??= hashCode; @@ -52,6 +54,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { _refresh(); } })); + favourites.addListener(onFavouritesChanged); } _refresh(); } @@ -61,7 +64,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); - _subscriptions = null; + favourites.removeListener(onFavouritesChanged); super.dispose(); } @@ -70,11 +73,11 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { int get entryCount => _filteredSortedEntries.length; // sorted as displayed to the user, i.e. sorted then grouped, not an absolute order on all entries - List _sortedEntries; + List? _sortedEntries; List get sortedEntries { - _sortedEntries ??= List.of(sections.entries.expand((e) => e.value)); - return _sortedEntries; + _sortedEntries ??= List.of(sections.entries.expand((kv) => kv.value)); + return _sortedEntries!; } bool get showHeaders { @@ -90,7 +93,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { } void addFilter(CollectionFilter filter) { - if (filter == null || filters.contains(filter)) return; + if (filters.contains(filter)) return; if (filter.isUnique) { filters.removeWhere((old) => old.category == filter.category); } @@ -99,7 +102,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { } void removeFilter(CollectionFilter filter) { - if (filter == null || !filters.contains(filter)) return; + if (!filters.contains(filter)) return; filters.remove(filter); onFilterChanged(); } @@ -156,19 +159,19 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { break; case EntryGroupFactor.none: sections = Map.fromEntries([ - MapEntry(null, _filteredSortedEntries), + MapEntry(const SectionKey(), _filteredSortedEntries), ]); break; } break; case EntrySortFactor.size: sections = Map.fromEntries([ - MapEntry(null, _filteredSortedEntries), + MapEntry(const SectionKey(), _filteredSortedEntries), ]); break; case EntrySortFactor.name: final byAlbum = groupBy(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory)); - sections = SplayTreeMap>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory, b.directory)); + sections = SplayTreeMap>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory!, b.directory!)); break; } sections = Map.unmodifiable(sections); @@ -184,7 +187,13 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { _applyGroup(); } - void onEntryAdded(Set entries) { + void onFavouritesChanged() { + if (filters.any((filter) => filter is FavouriteFilter)) { + _refresh(); + } + } + + void onEntryAdded(Set? entries) { _refresh(); } diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 9339e3c78..c4702d5cb 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -15,6 +15,7 @@ import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/services/image_op_events.dart'; import 'package:aves/services/services.dart'; +import 'package:collection/collection.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; @@ -31,7 +32,7 @@ mixin SourceBase { Stream get progressStream => _progressStreamController.stream; - void setProgress({@required int done, @required int total}) => _progressStreamController.add(ProgressEvent(done: done, total: total)); + void setProgress({required int done, required int total}) => _progressStreamController.add(ProgressEvent(done: done, total: total)); } abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { @@ -45,24 +46,24 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM // TODO TLAD use `Set.unmodifiable()` when possible Set get allEntries => Set.of(_rawEntries); - Set _visibleEntries; + Set? _visibleEntries; @override Set get visibleEntries { // TODO TLAD use `Set.unmodifiable()` when possible _visibleEntries ??= Set.of(_applyHiddenFilters(_rawEntries)); - return _visibleEntries; + return _visibleEntries!; } - List _sortedEntriesByDate; + List? _sortedEntriesByDate; @override List get sortedEntriesByDate { _sortedEntriesByDate ??= List.unmodifiable(visibleEntries.toList()..sort(AvesEntry.compareByDate)); - return _sortedEntriesByDate; + return _sortedEntriesByDate!; } - List _savedDates; + late List _savedDates; Future loadDates() async { final stopwatch = Stopwatch()..start(); @@ -75,7 +76,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM return hiddenFilters.isEmpty ? entries : entries.where((entry) => !hiddenFilters.any((filter) => filter.test(entry))); } - void _invalidate([Set entries]) { + void _invalidate([Set? entries]) { _visibleEntries = null; _sortedEntriesByDate = null; invalidateAlbumFilterSummary(entries: entries); @@ -91,7 +92,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } entries.forEach((entry) { final contentId = entry.contentId; - entry.catalogDateMillis = _savedDates.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null)?.dateMillis; + entry.catalogDateMillis = _savedDates.firstWhereOrNull((metadata) => metadata.contentId == contentId)?.dateMillis; }); _rawEntries.addAll(entries); _invalidate(entries); @@ -124,16 +125,16 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } Future _moveEntry(AvesEntry entry, Map newFields) async { - final oldContentId = entry.contentId; - final newContentId = newFields['contentId'] as int; + final oldContentId = entry.contentId!; + final newContentId = newFields['contentId'] as int?; entry.contentId = newContentId; // `dateModifiedSecs` changes when moving entries to another directory, // but it does not change when renaming the containing directory - if (newFields.containsKey('dateModifiedSecs')) entry.dateModifiedSecs = newFields['dateModifiedSecs'] as int; - if (newFields.containsKey('path')) entry.path = newFields['path'] as String; + if (newFields.containsKey('dateModifiedSecs')) entry.dateModifiedSecs = newFields['dateModifiedSecs'] as int?; + if (newFields.containsKey('path')) entry.path = newFields['path'] as String?; if (newFields.containsKey('uri')) entry.uri = newFields['uri'] as String; - if (newFields.containsKey('title') != null) entry.sourceTitle = newFields['title'] as String; + if (newFields.containsKey('title')) entry.sourceTitle = newFields['title'] as String?; entry.catalogMetadata = entry.catalogMetadata?.copyWith(contentId: newContentId); entry.addressDetails = entry.addressDetails?.copyWith(contentId: newContentId); @@ -152,6 +153,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM await _moveEntry(entry, newFields); entry.metadataChangeNotifier.notifyListeners(); + eventBus.fire(EntryMovedEvent({entry})); return true; } @@ -159,7 +161,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM final oldFilter = AlbumFilter(sourceAlbum, null); final pinned = settings.pinnedFilters.contains(oldFilter); final oldCoverContentId = covers.coverContentId(oldFilter); - final coverEntry = oldCoverContentId != null ? todoEntries.firstWhere((entry) => entry.contentId == oldCoverContentId, orElse: () => null) : null; + final coverEntry = oldCoverContentId != null ? todoEntries.firstWhereOrNull((entry) => entry.contentId == oldCoverContentId) : null; await updateAfterMove( todoEntries: todoEntries, copy: false, @@ -177,37 +179,39 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } Future updateAfterMove({ - @required Set todoEntries, - @required bool copy, - @required String destinationAlbum, - @required Set movedOps, + required Set todoEntries, + required bool copy, + required String destinationAlbum, + required Set movedOps, }) async { if (movedOps.isEmpty) return; - final fromAlbums = {}; + final fromAlbums = {}; final movedEntries = {}; if (copy) { movedOps.forEach((movedOp) { final sourceUri = movedOp.uri; final newFields = movedOp.newFields; - final sourceEntry = todoEntries.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null); - fromAlbums.add(sourceEntry.directory); - movedEntries.add(sourceEntry?.copyWith( - uri: newFields['uri'] as String, - path: newFields['path'] as String, - contentId: newFields['contentId'] as int, - dateModifiedSecs: newFields['dateModifiedSecs'] as int, - )); + final sourceEntry = todoEntries.firstWhereOrNull((entry) => entry.uri == sourceUri); + if (sourceEntry != null) { + fromAlbums.add(sourceEntry.directory); + movedEntries.add(sourceEntry.copyWith( + uri: newFields['uri'] as String?, + path: newFields['path'] as String?, + contentId: newFields['contentId'] as int?, + dateModifiedSecs: newFields['dateModifiedSecs'] as int?, + )); + } }); await metadataDb.saveEntries(movedEntries); - await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata)); - await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails)); + await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata).where((v) => v != null).cast().toSet()); + await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails).where((v) => v != null).cast().toSet()); } else { await Future.forEach(movedOps, (movedOp) async { final newFields = movedOp.newFields; if (newFields.isNotEmpty) { final sourceUri = movedOp.uri; - final entry = todoEntries.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null); + final entry = todoEntries.firstWhereOrNull((entry) => entry.uri == sourceUri); if (entry != null) { fromAlbums.add(entry.directory); movedEntries.add(entry); @@ -255,17 +259,17 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM return 0; } - AvesEntry recentEntry(CollectionFilter filter) { + AvesEntry? recentEntry(CollectionFilter filter) { if (filter is AlbumFilter) return albumRecentEntry(filter); if (filter is LocationFilter) return countryRecentEntry(filter); if (filter is TagFilter) return tagRecentEntry(filter); return null; } - AvesEntry coverEntry(CollectionFilter filter) { + AvesEntry? coverEntry(CollectionFilter filter) { final contentId = covers.coverContentId(filter); if (contentId != null) { - final entry = visibleEntries.firstWhere((entry) => entry.contentId == contentId, orElse: () => null); + final entry = visibleEntries.firstWhereOrNull((entry) => entry.contentId == contentId); if (entry != null) return entry; } return recentEntry(filter); @@ -297,7 +301,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } class EntryAddedEvent { - final Set entries; + final Set? entries; const EntryAddedEvent([this.entries]); } @@ -309,7 +313,7 @@ class EntryRemovedEvent { } class EntryMovedEvent { - final Iterable entries; + final Set entries; const EntryMovedEvent(this.entries); } @@ -324,5 +328,5 @@ class FilterVisibilityChangedEvent { class ProgressEvent { final int done, total; - const ProgressEvent({@required this.done, @required this.total}); + const ProgressEvent({required this.done, required this.total}); } diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index cfb178d04..26169e404 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -22,7 +22,7 @@ mixin LocationMixin on SourceBase { final saved = await metadataDb.loadAddresses(); visibleEntries.forEach((entry) { final contentId = entry.contentId; - entry.addressDetails = saved.firstWhere((address) => address.contentId == contentId, orElse: () => null); + entry.addressDetails = saved.firstWhereOrNull((address) => address.contentId == contentId); }); debugPrint('$runtimeType loadAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} entries'); onAddressMetadataChanged(); @@ -44,19 +44,19 @@ mixin LocationMixin on SourceBase { setProgress(done: progressDone, total: progressTotal); // final stopwatch = Stopwatch()..start(); - final countryCodeMap = await countryTopology.countryCodeMap(todo.map((entry) => entry.latLng).toSet()); + final countryCodeMap = await countryTopology.countryCodeMap(todo.map((entry) => entry.latLng!).toSet()); final newAddresses = []; todo.forEach((entry) { final position = entry.latLng; - final countryCode = countryCodeMap.entries.firstWhere((kv) => kv.value.contains(position), orElse: () => null)?.key; + final countryCode = countryCodeMap.entries.firstWhereOrNull((kv) => kv.value.contains(position))?.key; entry.setCountry(countryCode); if (entry.hasAddress) { - newAddresses.add(entry.addressDetails); + newAddresses.add(entry.addressDetails!); } setProgress(done: ++progressDone, total: progressTotal); }); if (newAddresses.isNotEmpty) { - await metadataDb.saveAddresses(List.unmodifiable(newAddresses)); + await metadataDb.saveAddresses(Set.of(newAddresses)); onAddressMetadataChanged(); } // debugPrint('$runtimeType _locateCountries complete in ${stopwatch.elapsed.inMilliseconds}ms'); @@ -82,14 +82,16 @@ mixin LocationMixin on SourceBase { // cf https://en.wikipedia.org/wiki/Decimal_degrees#Precision final latLngFactor = pow(10, 2); Tuple2 approximateLatLng(AvesEntry entry) { - final lat = entry.catalogMetadata?.latitude; - final lng = entry.catalogMetadata?.longitude; - if (lat == null || lng == null) return null; + // entry has coordinates + final lat = entry.catalogMetadata!.latitude!; + final lng = entry.catalogMetadata!.longitude!; return Tuple2((lat * latLngFactor).round(), (lng * latLngFactor).round()); } - final knownLocations = , AddressDetails>{}; - byLocated[true]?.forEach((entry) => knownLocations.putIfAbsent(approximateLatLng(entry), () => entry.addressDetails)); + final knownLocations = , AddressDetails?>{}; + byLocated[true]?.forEach((entry) { + knownLocations.putIfAbsent(approximateLatLng(entry), () => entry.addressDetails); + }); stateNotifier.value = SourceState.locating; var progressDone = 0; @@ -108,9 +110,9 @@ mixin LocationMixin on SourceBase { knownLocations[latLng] = entry.addressDetails; } if (entry.hasFineAddress) { - newAddresses.add(entry.addressDetails); + newAddresses.add(entry.addressDetails!); if (newAddresses.length >= _commitCountThreshold) { - await metadataDb.saveAddresses(List.unmodifiable(newAddresses)); + await metadataDb.saveAddresses(Set.of(newAddresses)); onAddressMetadataChanged(); newAddresses.clear(); } @@ -118,7 +120,7 @@ mixin LocationMixin on SourceBase { setProgress(done: ++progressDone, total: progressTotal); }); if (newAddresses.isNotEmpty) { - await metadataDb.saveAddresses(List.unmodifiable(newAddresses)); + await metadataDb.saveAddresses(Set.of(newAddresses)); onAddressMetadataChanged(); } // debugPrint('$runtimeType _locatePlaces complete in ${stopwatch.elapsed.inSeconds}s'); @@ -130,8 +132,8 @@ mixin LocationMixin on SourceBase { } void updateLocations() { - final locations = visibleEntries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails).toList(); - final updatedPlaces = locations.map((address) => address.place).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase); + final locations = visibleEntries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails).cast().toList(); + final updatedPlaces = locations.map((address) => address.place).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase as int Function(String?, String?)?); if (!listEquals(updatedPlaces, sortedPlaces)) { sortedPlaces = List.unmodifiable(updatedPlaces); eventBus.fire(PlacesChangedEvent()); @@ -140,7 +142,7 @@ mixin LocationMixin on SourceBase { // the same country code could be found with different country names // e.g. if the locale changed between geocoding calls // so we merge countries by code, keeping only one name for each code - final countriesByCode = Map.fromEntries(locations.map((address) => MapEntry(address.countryCode, address.countryName)).where((kv) => kv.key != null && kv.key.isNotEmpty)); + final countriesByCode = Map.fromEntries(locations.map((address) => MapEntry(address.countryCode, address.countryName)).where((kv) => kv.key != null && kv.key!.isNotEmpty)); final updatedCountries = countriesByCode.entries.map((kv) => '${kv.value}${LocationFilter.locationSeparator}${kv.key}').toList()..sort(compareAsciiUpperCase); if (!listEquals(updatedCountries, sortedCountries)) { sortedCountries = List.unmodifiable(updatedCountries); @@ -153,27 +155,30 @@ mixin LocationMixin on SourceBase { // by country code final Map _filterEntryCountMap = {}; - final Map _filterRecentEntryMap = {}; + final Map _filterRecentEntryMap = {}; - void invalidateCountryFilterSummary([Set entries]) { - Set countryCodes; + void invalidateCountryFilterSummary([Set? entries]) { + Set? countryCodes; if (entries == null) { _filterEntryCountMap.clear(); _filterRecentEntryMap.clear(); } else { - countryCodes = entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails.countryCode).toSet(); - countryCodes.remove(null); + countryCodes = entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails!.countryCode).where((v) => v != null).cast().toSet(); countryCodes.forEach(_filterEntryCountMap.remove); } eventBus.fire(CountrySummaryInvalidatedEvent(countryCodes)); } int countryEntryCount(LocationFilter filter) { - return _filterEntryCountMap.putIfAbsent(filter.countryCode, () => visibleEntries.where(filter.test).length); + final countryCode = filter.countryCode; + if (countryCode == null) return 0; + return _filterEntryCountMap.putIfAbsent(countryCode, () => visibleEntries.where(filter.test).length); } - AvesEntry countryRecentEntry(LocationFilter filter) { - return _filterRecentEntryMap.putIfAbsent(filter.countryCode, () => sortedEntriesByDate.firstWhere(filter.test, orElse: () => null)); + AvesEntry? countryRecentEntry(LocationFilter filter) { + final countryCode = filter.countryCode; + if (countryCode == null) return null; + return _filterRecentEntryMap.putIfAbsent(countryCode, () => sortedEntriesByDate.firstWhereOrNull(filter.test)); } } @@ -184,7 +189,7 @@ class PlacesChangedEvent {} class CountriesChangedEvent {} class CountrySummaryInvalidatedEvent { - final Set countryCodes; + final Set? countryCodes; const CountrySummaryInvalidatedEvent(this.countryCodes); } diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index b4e65fd2d..0a4919d78 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -10,6 +10,7 @@ import 'package:aves/model/source/enums.dart'; import 'package:aves/services/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/math_utils.dart'; +import 'package:collection/collection.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/material.dart'; @@ -27,13 +28,15 @@ class MediaStoreSource extends CollectionSource { await favourites.init(); await covers.init(); final currentTimeZone = await timeService.getDefaultTimeZone(); - final catalogTimeZone = settings.catalogTimeZone; - if (currentTimeZone != catalogTimeZone) { - // clear catalog metadata to get correct date/times when moving to a different time zone - debugPrint('$runtimeType clear catalog metadata to get correct date/times'); - await metadataDb.clearDates(); - await metadataDb.clearMetadataEntries(); - settings.catalogTimeZone = currentTimeZone; + if (currentTimeZone != null) { + final catalogTimeZone = settings.catalogTimeZone; + if (currentTimeZone != catalogTimeZone) { + // clear catalog metadata to get correct date/times when moving to a different time zone + debugPrint('$runtimeType clear catalog metadata to get correct date/times'); + await metadataDb.clearDates(); + await metadataDb.clearMetadataEntries(); + settings.catalogTimeZone = currentTimeZone; + } } await loadDates(); // 100ms for 5400 entries _initialized = true; @@ -49,7 +52,7 @@ class MediaStoreSource extends CollectionSource { clearEntries(); final oldEntries = await metadataDb.loadEntries(); // 400ms for 5500 entries - final knownDateById = Map.fromEntries(oldEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedSecs))); + final knownDateById = Map.fromEntries(oldEntries.map((entry) => MapEntry(entry.contentId!, entry.dateModifiedSecs!))); final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(knownDateById.keys.toList())).toSet(); oldEntries.removeWhere((entry) => obsoleteContentIds.contains(entry.contentId)); @@ -63,7 +66,7 @@ class MediaStoreSource extends CollectionSource { await metadataDb.removeIds(obsoleteContentIds, metadataOnly: false); // verify paths because some apps move files without updating their `last modified date` - final knownPathById = Map.fromEntries(allEntries.map((entry) => MapEntry(entry.contentId, entry.path))); + final knownPathById = Map.fromEntries(allEntries.map((entry) => MapEntry(entry.contentId!, entry.path))); final movedContentIds = (await mediaStoreService.checkObsoletePaths(knownPathById)).toSet(); movedContentIds.forEach((contentId) { // make obsolete by resetting its modified date @@ -130,20 +133,22 @@ class MediaStoreSource extends CollectionSource { Future> refreshUris(Set changedUris) async { if (!_initialized || !isMonitoring) return changedUris; - final uriByContentId = Map.fromEntries(changedUris.map((uri) { - if (uri == null) return null; - final pathSegments = Uri.parse(uri).pathSegments; - // e.g. URI `content://media/` has no path segment - if (pathSegments.isEmpty) return null; - final idString = pathSegments.last; - final contentId = int.tryParse(idString); - if (contentId == null) return null; - return MapEntry(contentId, uri); - }).where((kv) => kv != null)); + final uriByContentId = Map.fromEntries(changedUris + .map((uri) { + final pathSegments = Uri.parse(uri).pathSegments; + // e.g. URI `content://media/` has no path segment + if (pathSegments.isEmpty) return null; + final idString = pathSegments.last; + final contentId = int.tryParse(idString); + if (contentId == null) return null; + return MapEntry(contentId, uri); + }) + .where((kv) => kv != null) + .cast>()); // clean up obsolete entries final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(uriByContentId.keys.toList())).toSet(); - final obsoleteUris = obsoleteContentIds.map((contentId) => uriByContentId[contentId]).toSet(); + final obsoleteUris = obsoleteContentIds.map((contentId) => uriByContentId[contentId]).where((v) => v != null).cast().toSet(); await removeEntries(obsoleteUris); obsoleteContentIds.forEach(uriByContentId.remove); @@ -156,14 +161,16 @@ class MediaStoreSource extends CollectionSource { final uri = kv.value; final sourceEntry = await imageFileService.getEntry(uri, null); if (sourceEntry != null) { - final existingEntry = allEntries.firstWhere((entry) => entry.contentId == contentId, orElse: () => null); + final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId); // compare paths because some apps move files without updating their `last modified date` - if (existingEntry == null || sourceEntry.dateModifiedSecs > existingEntry.dateModifiedSecs || sourceEntry.path != existingEntry.path) { - final volume = androidFileUtils.getStorageVolume(sourceEntry.path); + if (existingEntry == null || (sourceEntry.dateModifiedSecs ?? 0) > (existingEntry.dateModifiedSecs ?? 0) || sourceEntry.path != existingEntry.path) { + final newPath = sourceEntry.path; + final volume = newPath != null ? androidFileUtils.getStorageVolume(newPath) : null; if (volume != null) { newEntries.add(sourceEntry); - if (existingEntry != null) { - existingDirectories.add(existingEntry.directory); + final existingDirectory = existingEntry?.directory; + if (existingDirectory != null) { + existingDirectories.add(existingDirectory); } } else { debugPrint('$runtimeType refreshUris entry=$sourceEntry is not located on a known storage volume. Will retry soon...'); @@ -189,7 +196,7 @@ class MediaStoreSource extends CollectionSource { @override Future refreshMetadata(Set entries) { final contentIds = entries.map((entry) => entry.contentId).toSet(); - metadataDb.removeIds(contentIds, metadataOnly: true); + metadataDb.removeIds(contentIds as Set, metadataOnly: true); return refresh(); } } diff --git a/lib/model/source/section_keys.dart b/lib/model/source/section_keys.dart index 66988f27d..be43543ee 100644 --- a/lib/model/source/section_keys.dart +++ b/lib/model/source/section_keys.dart @@ -5,7 +5,7 @@ class SectionKey { } class EntryAlbumSectionKey extends SectionKey { - final String directory; + final String? directory; const EntryAlbumSectionKey(this.directory); @@ -23,7 +23,7 @@ class EntryAlbumSectionKey extends SectionKey { } class EntryDateSectionKey extends SectionKey { - final DateTime date; + final DateTime? date; const EntryDateSectionKey(this.date); diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index dec9b0df3..bea5b66eb 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -17,7 +17,7 @@ mixin TagMixin on SourceBase { final saved = await metadataDb.loadMetadataEntries(); visibleEntries.forEach((entry) { final contentId = entry.contentId; - entry.catalogMetadata = saved.firstWhere((metadata) => metadata.contentId == contentId, orElse: () => null); + entry.catalogMetadata = saved.firstWhereOrNull((metadata) => metadata.contentId == contentId); }); debugPrint('$runtimeType loadCatalogMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} entries'); onCatalogMetadataChanged(); @@ -37,16 +37,16 @@ mixin TagMixin on SourceBase { await Future.forEach(todo, (entry) async { await entry.catalog(background: true); if (entry.isCatalogued) { - newMetadata.add(entry.catalogMetadata); + newMetadata.add(entry.catalogMetadata!); if (newMetadata.length >= _commitCountThreshold) { - await metadataDb.saveMetadata(List.unmodifiable(newMetadata)); + await metadataDb.saveMetadata(Set.of(newMetadata)); onCatalogMetadataChanged(); newMetadata.clear(); } } setProgress(done: ++progressDone, total: progressTotal); }); - await metadataDb.saveMetadata(List.unmodifiable(newMetadata)); + await metadataDb.saveMetadata(Set.of(newMetadata)); onCatalogMetadataChanged(); // debugPrint('$runtimeType catalogEntries complete in ${stopwatch.elapsed.inSeconds}s'); } @@ -69,10 +69,10 @@ mixin TagMixin on SourceBase { // by tag final Map _filterEntryCountMap = {}; - final Map _filterRecentEntryMap = {}; + final Map _filterRecentEntryMap = {}; - void invalidateTagFilterSummary([Set entries]) { - Set tags; + void invalidateTagFilterSummary([Set? entries]) { + Set? tags; if (entries == null) { _filterEntryCountMap.clear(); _filterRecentEntryMap.clear(); @@ -87,8 +87,8 @@ mixin TagMixin on SourceBase { return _filterEntryCountMap.putIfAbsent(filter.tag, () => visibleEntries.where(filter.test).length); } - AvesEntry tagRecentEntry(TagFilter filter) { - return _filterRecentEntryMap.putIfAbsent(filter.tag, () => sortedEntriesByDate.firstWhere(filter.test, orElse: () => null)); + AvesEntry? tagRecentEntry(TagFilter filter) { + return _filterRecentEntryMap.putIfAbsent(filter.tag, () => sortedEntriesByDate.firstWhereOrNull(filter.test)); } } @@ -97,7 +97,7 @@ class CatalogMetadataChangedEvent {} class TagsChangedEvent {} class TagSummaryInvalidatedEvent { - final Set tags; + final Set? tags; const TagSummaryInvalidatedEvent(this.tags); } diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index c1851231f..72c145de8 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -11,6 +11,8 @@ import 'package:aves/utils/math_utils.dart'; import 'package:aves/utils/string_utils.dart'; import 'package:aves/utils/time_utils.dart'; import 'package:aves/widgets/viewer/video/fijkplayer.dart'; +import 'package:collection/collection.dart'; +// ignore: import_of_legacy_library_into_null_safe import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/foundation.dart'; @@ -54,16 +56,16 @@ class VideoMetadataFormatter { final value = kv.value; if (value != null) { try { - String key; - String keyLanguage; + String? key; + String? keyLanguage; // some keys have a language suffix, but they may be duplicates // we only keep the root key when they have the same value as the same key with no language final languageMatch = keyWithLanguagePattern.firstMatch(kv.key); if (languageMatch != null) { - final code = languageMatch.group(2); + final code = languageMatch.group(2)!; final native = _formatLanguage(code); if (native != code) { - final root = languageMatch.group(1); + final root = languageMatch.group(1)!; final rootValue = info[root]; // skip if it is a duplicate of the same entry with no language if (rootValue == value) continue; @@ -76,7 +78,7 @@ class VideoMetadataFormatter { } key = (key ?? (kv.key as String)).toLowerCase(); - void save(String key, String value) { + void save(String key, String? value) { if (value != null) { dir[keyLanguage != null ? '$key ($keyLanguage)' : key] = value; } @@ -129,21 +131,26 @@ class VideoMetadataFormatter { case Keys.codecProfileId: if (codec == 'h264') { final profile = int.tryParse(value); - if (profile != null && profile != 0) { - final level = int.tryParse(info[Keys.codecLevel]); + final levelString = info[Keys.codecLevel]; + if (profile != null && profile != 0 && levelString != null) { + final level = int.tryParse(levelString) ?? 0; save('Codec Profile', H264.formatProfile(profile, level)); } } break; case Keys.compatibleBrands: - save('Compatible Brands', RegExp(r'.{4}').allMatches(value).map((m) => _formatBrand(m.group(0))).join(', ')); + final formattedBrands = RegExp(r'.{4}').allMatches(value).map((m) { + final brand = m.group(0)!; + return _formatBrand(brand); + }).join(', '); + save('Compatible Brands', formattedBrands); break; case Keys.creationTime: save('Creation Time', _formatDate(value)); break; case Keys.date: - if (value != '0') { - final charCount = (value as String)?.length ?? 0; + if (value is String && value != '0') { + final charCount = value.length; save(charCount == 4 ? 'Year' : 'Date', value); } break; @@ -222,10 +229,10 @@ class VideoMetadataFormatter { static String _formatChannelLayout(value) => ChannelLayouts.names[value] ?? 'unknown ($value)'; - static String _formatCodecName(String value) => _codecNames[value] ?? value?.toUpperCase()?.replaceAll('_', ' '); + static String _formatCodecName(String value) => _codecNames[value] ?? value.toUpperCase().replaceAll('_', ' '); // input example: '2021-04-12T09:14:37.000000Z' - static String _formatDate(String value) { + static String? _formatDate(String value) { final date = DateTime.tryParse(value); if (date == null) return value; if (date == _epoch) return null; @@ -236,10 +243,10 @@ class VideoMetadataFormatter { static String _formatDuration(String value) { final match = _durationPattern.firstMatch(value); if (match != null) { - final h = int.tryParse(match.group(1)); - final m = int.tryParse(match.group(2)); - final s = int.tryParse(match.group(3)); - final millis = double.tryParse(match.group(4)); + final h = int.tryParse(match.group(1)!); + final m = int.tryParse(match.group(2)!); + final s = int.tryParse(match.group(3)!); + final millis = double.tryParse(match.group(4)!); if (h != null && m != null && s != null && millis != null) { return formatPreciseDuration(Duration( hours: h, @@ -258,15 +265,15 @@ class VideoMetadataFormatter { } static String _formatLanguage(String value) { - final language = Language.living639_2.firstWhere((language) => language.iso639_2 == value, orElse: () => null); + final language = Language.living639_2.firstWhereOrNull((language) => language.iso639_2 == value); return language?.native ?? value; } // format ISO 6709 input, e.g. '+37.5090+127.0243/' (Samsung), '+51.3328-000.7053+113.474/' (Apple) - static String _formatLocation(String value) { + static String? _formatLocation(String value) { final matches = _locationPattern.allMatches(value); if (matches.isNotEmpty) { - final coordinates = matches.map((m) => double.tryParse(m.group(0))).toList(); + final coordinates = matches.map((m) => double.tryParse(m.group(0)!)).toList(); if (coordinates.every((c) => c == 0)) return null; return coordinates.join(', '); } diff --git a/lib/ref/brand_colors.dart b/lib/ref/brand_colors.dart index 17db1afd0..b175781b2 100644 --- a/lib/ref/brand_colors.dart +++ b/lib/ref/brand_colors.dart @@ -7,17 +7,15 @@ class BrandColors { static const Color android = Color(0xFF3DDC84); static const Color flutter = Color(0xFF47D1FD); - static Color get(String text) { - if (text != null) { - switch (text.toLowerCase()) { - case 'after effects': - return adobeAfterEffects; - case 'illustrator': - return adobeIllustrator; - case 'photoshop': - case 'lightroom': - return adobePhotoshop; - } + static Color? get(String text) { + switch (text.toLowerCase()) { + case 'after effects': + return adobeAfterEffects; + case 'illustrator': + return adobeIllustrator; + case 'photoshop': + case 'lightroom': + return adobePhotoshop; } return null; } diff --git a/lib/ref/exif.dart b/lib/ref/exif.dart index 1c6b890cf..26fa9bb6d 100644 --- a/lib/ref/exif.dart +++ b/lib/ref/exif.dart @@ -133,7 +133,7 @@ class Exif { } static String getExifVersionDescription(String valueString) { - if (valueString?.length == 4) { + if (valueString.length == 4) { final major = int.tryParse(valueString.substring(0, 2)); final minor = int.tryParse(valueString.substring(2, 4)); if (major != null && minor != null) { diff --git a/lib/ref/languages.dart b/lib/ref/languages.dart index 5deab364a..a26cc982f 100644 --- a/lib/ref/languages.dart +++ b/lib/ref/languages.dart @@ -1,9 +1,10 @@ class Language { - final String iso639_2, name, native; + final String iso639_2, name; + final String? native; const Language({ - this.iso639_2, - this.name, + required this.iso639_2, + required this.name, this.native, }); diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index 38ae05445..4b1f50d2e 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.dart @@ -14,7 +14,7 @@ class AndroidAppService { final result = await platform.invokeMethod('getPackages'); final packages = (result as List).cast().map((map) => Package.fromMap(map)).toSet(); // additional info for known directories - final kakaoTalk = packages.firstWhere((package) => package.packageName == 'com.kakao.talk', orElse: () => null); + final kakaoTalk = packages.firstWhereOrNull((package) => package.packageName == 'com.kakao.talk'); if (kakaoTalk != null) { kakaoTalk.ownedDirs.add('KakaoTalkDownload'); } @@ -31,19 +31,20 @@ class AndroidAppService { 'packageName': packageName, 'sizeDip': size, }); - return result as Uint8List; + if (result != null) return result as Uint8List; } on PlatformException catch (e) { debugPrint('getAppIcon failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } - return null; + return Uint8List(0); } static Future edit(String uri, String mimeType) async { try { - return await platform.invokeMethod('edit', { + final result = await platform.invokeMethod('edit', { 'uri': uri, 'mimeType': mimeType, }); + if (result != null) return result as bool; } on PlatformException catch (e) { debugPrint('edit failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -52,10 +53,11 @@ class AndroidAppService { static Future open(String uri, String mimeType) async { try { - return await platform.invokeMethod('open', { + final result = await platform.invokeMethod('open', { 'uri': uri, 'mimeType': mimeType, }); + if (result != null) return result as bool; } on PlatformException catch (e) { debugPrint('open failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -64,9 +66,10 @@ class AndroidAppService { static Future openMap(String geoUri) async { try { - return await platform.invokeMethod('openMap', { + final result = await platform.invokeMethod('openMap', { 'geoUri': geoUri, }); + if (result != null) return result as bool; } on PlatformException catch (e) { debugPrint('openMap failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -75,10 +78,11 @@ class AndroidAppService { static Future setAs(String uri, String mimeType) async { try { - return await platform.invokeMethod('setAs', { + final result = await platform.invokeMethod('setAs', { 'uri': uri, 'mimeType': mimeType, }); + if (result != null) return result as bool; } on PlatformException catch (e) { debugPrint('setAs failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -90,9 +94,10 @@ class AndroidAppService { // e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats final urisByMimeType = groupBy(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())); try { - return await platform.invokeMethod('share', { + final result = await platform.invokeMethod('share', { 'urisByMimeType': urisByMimeType, }); + if (result != null) return result as bool; } on PlatformException catch (e) { debugPrint('shareEntries failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -101,11 +106,12 @@ class AndroidAppService { static Future shareSingle(String uri, String mimeType) async { try { - return await platform.invokeMethod('share', { + final result = await platform.invokeMethod('share', { 'urisByMimeType': { mimeType: [uri] }, }); + if (result != null) return result as bool; } on PlatformException catch (e) { debugPrint('shareSingle failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } diff --git a/lib/services/android_debug_service.dart b/lib/services/android_debug_service.dart index cf89f8423..257e79be2 100644 --- a/lib/services/android_debug_service.dart +++ b/lib/services/android_debug_service.dart @@ -9,7 +9,7 @@ class AndroidDebugService { static Future getContextDirs() async { try { final result = await platform.invokeMethod('getContextDirs'); - return result as Map; + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getContextDirs failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } @@ -19,7 +19,7 @@ class AndroidDebugService { static Future getEnv() async { try { final result = await platform.invokeMethod('getEnv'); - return result as Map; + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getEnv failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } @@ -31,8 +31,8 @@ class AndroidDebugService { // returns map with all data available when decoding image bounds with `BitmapFactory` final result = await platform.invokeMethod('getBitmapFactoryInfo', { 'uri': entry.uri, - }) as Map; - return result; + }); + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getBitmapFactoryInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -45,8 +45,8 @@ class AndroidDebugService { final result = await platform.invokeMethod('getContentResolverMetadata', { 'mimeType': entry.mimeType, 'uri': entry.uri, - }) as Map; - return result; + }); + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getContentResolverMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -60,8 +60,8 @@ class AndroidDebugService { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, - }) as Map; - return result; + }); + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getExifInterfaceMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -73,8 +73,8 @@ class AndroidDebugService { // returns map with all data available from `MediaMetadataRetriever` final result = await platform.invokeMethod('getMediaMetadataRetrieverMetadata', { 'uri': entry.uri, - }) as Map; - return result; + }); + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getMediaMetadataRetrieverMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -88,8 +88,8 @@ class AndroidDebugService { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, - }) as Map; - return result; + }); + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getMetadataExtractorSummary failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -102,8 +102,8 @@ class AndroidDebugService { try { final result = await platform.invokeMethod('getTiffStructure', { 'uri': entry.uri, - }) as Map; - return result; + }); + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getTiffStructure failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } diff --git a/lib/services/app_shortcut_service.dart b/lib/services/app_shortcut_service.dart index d13daa4cb..c81d9a7f8 100644 --- a/lib/services/app_shortcut_service.dart +++ b/lib/services/app_shortcut_service.dart @@ -10,24 +10,27 @@ class AppShortcutService { static const platform = MethodChannel('deckers.thibault/aves/shortcut'); // this ability will not change over the lifetime of the app - static bool _canPin; + static bool? _canPin; - static Future canPin() async { + static Future canPin() async { if (_canPin != null) { - return SynchronousFuture(_canPin); + return SynchronousFuture(_canPin!); } try { - _canPin = await platform.invokeMethod('canPin'); - return _canPin; + final result = await platform.invokeMethod('canPin'); + if (result != null) { + _canPin = result; + return result; + } } on PlatformException catch (e) { debugPrint('canPin failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } return false; } - static Future pin(String label, AvesEntry entry, Set filters) async { - Uint8List iconBytes; + static Future pin(String label, AvesEntry? entry, Set filters) async { + Uint8List? iconBytes; if (entry != null) { final size = entry.isVideo ? 0.0 : 256.0; iconBytes = await imageFileService.getThumbnail( @@ -44,7 +47,7 @@ class AppShortcutService { await platform.invokeMethod('pin', { 'label': label, 'iconBytes': iconBytes, - 'filters': filters.where((filter) => filter != null).map((filter) => filter.toJson()).toList(), + 'filters': filters.map((filter) => filter.toJson()).toList(), }); } on PlatformException catch (e) { debugPrint('pin failed with code=${e.code}, exception=${e.message}, details=${e.details}'); diff --git a/lib/services/embedded_data_service.dart b/lib/services/embedded_data_service.dart index b92aa2410..50cd15adf 100644 --- a/lib/services/embedded_data_service.dart +++ b/lib/services/embedded_data_service.dart @@ -11,7 +11,7 @@ abstract class EmbeddedDataService { Future extractVideoEmbeddedPicture(AvesEntry entry); - Future extractXmpDataProp(AvesEntry entry, String propPath, String propMimeType); + Future extractXmpDataProp(AvesEntry entry, String? propPath, String? propMimeType); } class PlatformEmbeddedDataService implements EmbeddedDataService { @@ -25,7 +25,7 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, }); - return (result as List).cast(); + if (result != null) return (result as List).cast(); } on PlatformException catch (e) { debugPrint('getExifThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -41,11 +41,11 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { 'sizeBytes': entry.sizeBytes, 'displayName': '${entry.bestTitle} • Video', }); - return result; + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('extractMotionPhotoVideo failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } - return null; + return {}; } @override @@ -55,15 +55,15 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { 'uri': entry.uri, 'displayName': '${entry.bestTitle} • Cover', }); - return result; + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('extractVideoEmbeddedPicture failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } - return null; + return {}; } @override - Future extractXmpDataProp(AvesEntry entry, String propPath, String propMimeType) async { + Future extractXmpDataProp(AvesEntry entry, String? propPath, String? propMimeType) async { try { final result = await platform.invokeMethod('extractXmpDataProp', { 'mimeType': entry.mimeType, @@ -73,10 +73,10 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { 'propPath': propPath, 'propMimeType': propMimeType, }); - return result; + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('extractXmpDataProp failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } - return null; + return {}; } } diff --git a/lib/services/geocoding_service.dart b/lib/services/geocoding_service.dart index 3d69a01cf..c87baddf2 100644 --- a/lib/services/geocoding_service.dart +++ b/lib/services/geocoding_service.dart @@ -29,7 +29,7 @@ class GeocodingService { @immutable class Address { - final String addressLine, adminArea, countryCode, countryName, featureName, locality, postalCode, subAdminArea, subLocality, subThoroughfare, thoroughfare; + final String? addressLine, adminArea, countryCode, countryName, featureName, locality, postalCode, subAdminArea, subLocality, subThoroughfare, thoroughfare; const Address({ this.addressLine, diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index 4bf6238e8..dc66affb3 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -7,28 +7,30 @@ import 'package:aves/model/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/image_op_events.dart'; import 'package:aves/services/service_policy.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +// ignore: import_of_legacy_library_into_null_safe import 'package:streams_channel/streams_channel.dart'; abstract class ImageFileService { - Future getEntry(String uri, String mimeType); + Future getEntry(String uri, String? mimeType); Future getSvg( String uri, String mimeType, { - int expectedContentLength, - BytesReceivedCallback onBytesReceived, + int? expectedContentLength, + BytesReceivedCallback? onBytesReceived, }); Future getImage( String uri, String mimeType, - int rotationDegrees, + int? rotationDegrees, bool isFlipped, { - int pageId, - int expectedContentLength, - BytesReceivedCallback onBytesReceived, + int? pageId, + int? expectedContentLength, + BytesReceivedCallback? onBytesReceived, }); // `rect`: region to decode, with coordinates in reference to `imageSize` @@ -40,21 +42,21 @@ abstract class ImageFileService { int sampleSize, Rectangle regionRect, Size imageSize, { - int pageId, - Object taskKey, - int priority, + int? pageId, + Object? taskKey, + int? priority, }); Future getThumbnail({ - @required String uri, - @required String mimeType, - @required int rotationDegrees, - @required int pageId, - @required bool isFlipped, - @required int dateModifiedSecs, - @required double extent, - Object taskKey, - int priority, + required String uri, + required String mimeType, + required int rotationDegrees, + required int? pageId, + required bool isFlipped, + required int? dateModifiedSecs, + required double extent, + Object? taskKey, + int? priority, }); Future clearSizedThumbnailDiskCache(); @@ -63,27 +65,27 @@ abstract class ImageFileService { bool cancelThumbnail(Object taskKey); - Future resumeLoading(Object taskKey); + Future? resumeLoading(Object taskKey); Stream delete(Iterable entries); Stream move( Iterable entries, { - @required bool copy, - @required String destinationAlbum, + required bool copy, + required String destinationAlbum, }); Stream export( Iterable entries, { - @required String mimeType, - @required String destinationAlbum, + required String mimeType, + required String destinationAlbum, }); - Future rename(AvesEntry entry, String newName); + Future> rename(AvesEntry entry, String newName); - Future rotate(AvesEntry entry, {@required bool clockwise}); + Future> rotate(AvesEntry entry, {required bool clockwise}); - Future flip(AvesEntry entry); + Future> flip(AvesEntry entry); } class PlatformImageFileService implements ImageFileService { @@ -108,7 +110,7 @@ class PlatformImageFileService implements ImageFileService { } @override - Future getEntry(String uri, String mimeType) async { + Future getEntry(String uri, String? mimeType) async { try { final result = await platform.invokeMethod('getEntry', { 'uri': uri, @@ -125,8 +127,8 @@ class PlatformImageFileService implements ImageFileService { Future getSvg( String uri, String mimeType, { - int expectedContentLength, - BytesReceivedCallback onBytesReceived, + int? expectedContentLength, + BytesReceivedCallback? onBytesReceived, }) => getImage( uri, @@ -141,11 +143,11 @@ class PlatformImageFileService implements ImageFileService { Future getImage( String uri, String mimeType, - int rotationDegrees, + int? rotationDegrees, bool isFlipped, { - int pageId, - int expectedContentLength, - BytesReceivedCallback onBytesReceived, + int? pageId, + int? expectedContentLength, + BytesReceivedCallback? onBytesReceived, }) { try { final completer = Completer.sync(); @@ -155,7 +157,7 @@ class PlatformImageFileService implements ImageFileService { 'uri': uri, 'mimeType': mimeType, 'rotationDegrees': rotationDegrees ?? 0, - 'isFlipped': isFlipped ?? false, + 'isFlipped': isFlipped, 'pageId': pageId, }).listen( (data) { @@ -182,7 +184,7 @@ class PlatformImageFileService implements ImageFileService { } on PlatformException catch (e) { debugPrint('getImage failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } - return Future.sync(() => null); + return Future.sync(() => Uint8List(0)); } @override @@ -194,9 +196,9 @@ class PlatformImageFileService implements ImageFileService { int sampleSize, Rectangle regionRect, Size imageSize, { - int pageId, - Object taskKey, - int priority, + int? pageId, + Object? taskKey, + int? priority, }) { return servicePolicy.call( () async { @@ -213,11 +215,11 @@ class PlatformImageFileService implements ImageFileService { 'imageWidth': imageSize.width.toInt(), 'imageHeight': imageSize.height.toInt(), }); - return result as Uint8List; + if (result != null) return result as Uint8List; } on PlatformException catch (e) { debugPrint('getRegion failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } - return null; + return Uint8List(0); }, priority: priority ?? ServiceCallPriority.getRegion, key: taskKey, @@ -226,18 +228,18 @@ class PlatformImageFileService implements ImageFileService { @override Future getThumbnail({ - @required String uri, - @required String mimeType, - @required int rotationDegrees, - @required int pageId, - @required bool isFlipped, - @required int dateModifiedSecs, - @required double extent, - Object taskKey, - int priority, + required String uri, + required String mimeType, + required int rotationDegrees, + required int? pageId, + required bool isFlipped, + required int? dateModifiedSecs, + required double extent, + Object? taskKey, + int? priority, }) { if (mimeType == MimeTypes.svg) { - return Future.sync(() => null); + return Future.sync(() => Uint8List(0)); } return servicePolicy.call( () async { @@ -253,11 +255,11 @@ class PlatformImageFileService implements ImageFileService { 'pageId': pageId, 'defaultSizeDip': thumbnailDefaultSize, }); - return result as Uint8List; + if (result != null) return result as Uint8List; } on PlatformException catch (e) { debugPrint('getThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } - return null; + return Uint8List(0); }, priority: priority ?? (extent == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail), key: taskKey, @@ -280,7 +282,7 @@ class PlatformImageFileService implements ImageFileService { bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getFastThumbnail, ServiceCallPriority.getSizedThumbnail]); @override - Future resumeLoading(Object taskKey) => servicePolicy.resume(taskKey); + Future? resumeLoading(Object taskKey) => servicePolicy.resume(taskKey); @override Stream delete(Iterable entries) { @@ -298,8 +300,8 @@ class PlatformImageFileService implements ImageFileService { @override Stream move( Iterable entries, { - @required bool copy, - @required String destinationAlbum, + required bool copy, + required String destinationAlbum, }) { try { return _opStreamChannel.receiveBroadcastStream({ @@ -317,8 +319,8 @@ class PlatformImageFileService implements ImageFileService { @override Stream export( Iterable entries, { - @required String mimeType, - @required String destinationAlbum, + required String mimeType, + required String destinationAlbum, }) { try { return _opStreamChannel.receiveBroadcastStream({ @@ -334,14 +336,14 @@ class PlatformImageFileService implements ImageFileService { } @override - Future rename(AvesEntry entry, String newName) async { + Future> rename(AvesEntry entry, String newName) async { try { // returns map with: 'contentId' 'path' 'title' 'uri' (all optional) final result = await platform.invokeMethod('rename', { 'entry': _toPlatformEntryMap(entry), 'newName': newName, - }) as Map; - return result; + }); + if (result != null) return (result as Map).cast(); } on PlatformException catch (e) { debugPrint('rename failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -349,14 +351,14 @@ class PlatformImageFileService implements ImageFileService { } @override - Future rotate(AvesEntry entry, {@required bool clockwise}) async { + Future> rotate(AvesEntry entry, {required bool clockwise}) async { try { // returns map with: 'rotationDegrees' 'isFlipped' final result = await platform.invokeMethod('rotate', { 'entry': _toPlatformEntryMap(entry), 'clockwise': clockwise, - }) as Map; - return result; + }); + if (result != null) return (result as Map).cast(); } on PlatformException catch (e) { debugPrint('rotate failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -364,13 +366,13 @@ class PlatformImageFileService implements ImageFileService { } @override - Future flip(AvesEntry entry) async { + Future> flip(AvesEntry entry) async { try { // returns map with: 'rotationDegrees' 'isFlipped' final result = await platform.invokeMethod('flip', { 'entry': _toPlatformEntryMap(entry), - }) as Map; - return result; + }); + if (result != null) return (result as Map).cast(); } on PlatformException catch (e) { debugPrint('flip failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -379,18 +381,18 @@ class PlatformImageFileService implements ImageFileService { } // cf flutter/foundation `consolidateHttpClientResponseBytes` -typedef BytesReceivedCallback = void Function(int cumulative, int total); +typedef BytesReceivedCallback = void Function(int cumulative, int? total); // cf flutter/foundation `consolidateHttpClientResponseBytes` class _OutputBuffer extends ByteConversionSinkBase { - List> _chunks = >[]; + List>? _chunks = >[]; int _contentLength = 0; - Uint8List _bytes; + Uint8List? _bytes; @override void add(List chunk) { assert(_bytes == null); - _chunks.add(chunk); + _chunks!.add(chunk); _contentLength += chunk.length; } @@ -402,8 +404,8 @@ class _OutputBuffer extends ByteConversionSinkBase { } _bytes = Uint8List(_contentLength); var offset = 0; - for (final chunk in _chunks) { - _bytes.setRange(offset, offset + chunk.length, chunk); + for (final chunk in _chunks!) { + _bytes!.setRange(offset, offset + chunk.length, chunk); offset += chunk.length; } _chunks = null; @@ -411,6 +413,6 @@ class _OutputBuffer extends ByteConversionSinkBase { Uint8List get bytes { assert(_bytes != null); - return _bytes; + return _bytes!; } } diff --git a/lib/services/image_op_events.dart b/lib/services/image_op_events.dart index 2f30d8fe7..6d172f8a1 100644 --- a/lib/services/image_op_events.dart +++ b/lib/services/image_op_events.dart @@ -7,8 +7,8 @@ class ImageOpEvent { final String uri; const ImageOpEvent({ - this.success, - this.uri, + required this.success, + required this.uri, }); factory ImageOpEvent.fromMap(Map map) { @@ -34,7 +34,7 @@ class ImageOpEvent { class MoveOpEvent extends ImageOpEvent { final Map newFields; - const MoveOpEvent({bool success, String uri, this.newFields}) + const MoveOpEvent({required bool success, required String uri, required this.newFields}) : super( success: success, uri: uri, @@ -44,7 +44,7 @@ class MoveOpEvent extends ImageOpEvent { return MoveOpEvent( success: map['success'] ?? false, uri: map['uri'], - newFields: map['newFields'], + newFields: map['newFields'] ?? {}, ); } @@ -53,9 +53,9 @@ class MoveOpEvent extends ImageOpEvent { } class ExportOpEvent extends MoveOpEvent { - final int pageId; + final int? pageId; - const ExportOpEvent({bool success, String uri, this.pageId, Map newFields}) + const ExportOpEvent({required bool success, required String uri, this.pageId, required Map newFields}) : super( success: success, uri: uri, @@ -67,7 +67,7 @@ class ExportOpEvent extends MoveOpEvent { success: map['success'] ?? false, uri: map['uri'], pageId: map['pageId'], - newFields: map['newFields'], + newFields: map['newFields'] ?? {}, ); } diff --git a/lib/services/media_store_service.dart b/lib/services/media_store_service.dart index edd6e134b..0bfd42d1a 100644 --- a/lib/services/media_store_service.dart +++ b/lib/services/media_store_service.dart @@ -3,12 +3,13 @@ import 'dart:async'; import 'package:aves/model/entry.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +// ignore: import_of_legacy_library_into_null_safe import 'package:streams_channel/streams_channel.dart'; abstract class MediaStoreService { Future> checkObsoleteContentIds(List knownContentIds); - Future> checkObsoletePaths(Map knownPathById); + Future> checkObsoletePaths(Map knownPathById); // knownEntries: map of contentId -> dateModifiedSecs Stream getEntries(Map knownEntries); @@ -32,7 +33,7 @@ class PlatformMediaStoreService implements MediaStoreService { } @override - Future> checkObsoletePaths(Map knownPathById) async { + Future> checkObsoletePaths(Map knownPathById) async { try { final result = await platform.invokeMethod('checkObsoletePaths', { 'knownPathById': knownPathById, diff --git a/lib/services/metadata_service.dart b/lib/services/metadata_service.dart index e5f2eabe6..bb77482dc 100644 --- a/lib/services/metadata_service.dart +++ b/lib/services/metadata_service.dart @@ -10,15 +10,15 @@ abstract class MetadataService { // returns Map> (map of directories, each directory being a map of metadata label and value description) Future getAllMetadata(AvesEntry entry); - Future getCatalogMetadata(AvesEntry entry, {bool background = false}); + Future getCatalogMetadata(AvesEntry entry, {bool background = false}); - Future getOverlayMetadata(AvesEntry entry); + Future getOverlayMetadata(AvesEntry entry); - Future getMultiPageInfo(AvesEntry entry); + Future getMultiPageInfo(AvesEntry entry); - Future getPanoramaInfo(AvesEntry entry); + Future getPanoramaInfo(AvesEntry entry); - Future getContentResolverProp(AvesEntry entry, String prop); + Future getContentResolverProp(AvesEntry entry, String prop); } class PlatformMetadataService implements MetadataService { @@ -26,7 +26,7 @@ class PlatformMetadataService implements MetadataService { @override Future getAllMetadata(AvesEntry entry) async { - if (entry.isSvg) return null; + if (entry.isSvg) return {}; try { final result = await platform.invokeMethod('getAllMetadata', { @@ -34,7 +34,7 @@ class PlatformMetadataService implements MetadataService { 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, }); - return result as Map; + if (result != null) return result as Map; } on PlatformException catch (e) { debugPrint('getAllMetadata failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } @@ -42,10 +42,10 @@ class PlatformMetadataService implements MetadataService { } @override - Future getCatalogMetadata(AvesEntry entry, {bool background = false}) async { + Future getCatalogMetadata(AvesEntry entry, {bool background = false}) async { if (entry.isSvg) return null; - Future call() async { + Future call() async { try { // returns map with: // 'mimeType': MIME type as reported by metadata extractors, not Media Store (string) @@ -80,7 +80,7 @@ class PlatformMetadataService implements MetadataService { } @override - Future getOverlayMetadata(AvesEntry entry) async { + Future getOverlayMetadata(AvesEntry entry) async { if (entry.isSvg) return null; try { @@ -98,7 +98,7 @@ class PlatformMetadataService implements MetadataService { } @override - Future getMultiPageInfo(AvesEntry entry) async { + Future getMultiPageInfo(AvesEntry entry) async { try { final result = await platform.invokeMethod('getMultiPageInfo', { 'mimeType': entry.mimeType, @@ -120,7 +120,7 @@ class PlatformMetadataService implements MetadataService { } @override - Future getPanoramaInfo(AvesEntry entry) async { + Future getPanoramaInfo(AvesEntry entry) async { try { // returns map with values for: // 'croppedAreaLeft' (int), 'croppedAreaTop' (int), 'croppedAreaWidth' (int), 'croppedAreaHeight' (int), @@ -138,7 +138,7 @@ class PlatformMetadataService implements MetadataService { } @override - Future getContentResolverProp(AvesEntry entry, String prop) async { + Future getContentResolverProp(AvesEntry entry, String prop) async { try { return await platform.invokeMethod('getContentResolverProp', { 'mimeType': entry.mimeType, diff --git a/lib/services/service_policy.dart b/lib/services/service_policy.dart index c4521b22a..bc3e4321f 100644 --- a/lib/services/service_policy.dart +++ b/lib/services/service_policy.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:collection'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:tuple/tuple.dart'; @@ -22,19 +23,19 @@ class ServicePolicy { Future call( Future Function() platformCall, { int priority = ServiceCallPriority.normal, - Object key, + Object? key, }) { Completer completer; - _Task task; + _Task task; key ??= platformCall.hashCode; final toResume = _paused.remove(key); if (toResume != null) { priority = toResume.item1; - task = toResume.item2; + task = toResume.item2 as _Task; completer = task.completer; } else { completer = Completer(); - task = _Task( + task = _Task( () async { try { completer.complete(await platformCall()); @@ -52,11 +53,11 @@ class ServicePolicy { return completer.future; } - Future resume(Object key) { + Future? resume(Object key) { final toResume = _paused.remove(key); if (toResume != null) { final priority = toResume.item1; - final task = toResume.item2; + final task = toResume.item2 as _Task; _getQueue(priority)[key] = task; _pickNext(); return task.completer.future; @@ -70,10 +71,10 @@ class ServicePolicy { void _pickNext() { _notifyQueueState(); if (_runningQueue.length >= concurrentTaskMax) return; - final queue = _queues.entries.firstWhere((kv) => kv.value.isNotEmpty, orElse: () => null)?.value; + final queue = _queues.entries.firstWhereOrNull((kv) => kv.value.isNotEmpty)?.value; if (queue != null && queue.isNotEmpty) { final key = queue.keys.first; - final task = queue.remove(key); + final task = queue.remove(key)!; _runningQueue[key] = task; task.callback(); } @@ -109,9 +110,9 @@ class ServicePolicy { } } -class _Task { +class _Task { final VoidCallback callback; - final Completer completer; + final Completer completer; const _Task(this.callback, this.completer); } diff --git a/lib/services/services.dart b/lib/services/services.dart index c863b0a36..1ffdf90b6 100644 --- a/lib/services/services.dart +++ b/lib/services/services.dart @@ -11,16 +11,16 @@ import 'package:path/path.dart' as p; final getIt = GetIt.instance; -final pContext = getIt(); -final availability = getIt(); -final metadataDb = getIt(); +final p.Context pContext = getIt(); +final AvesAvailability availability = getIt(); +final MetadataDb metadataDb = getIt(); -final embeddedDataService = getIt(); -final imageFileService = getIt(); -final mediaStoreService = getIt(); -final metadataService = getIt(); -final storageService = getIt(); -final timeService = getIt(); +final EmbeddedDataService embeddedDataService = getIt(); +final ImageFileService imageFileService = getIt(); +final MediaStoreService mediaStoreService = getIt(); +final MetadataService metadataService = getIt(); +final StorageService storageService = getIt(); +final TimeService timeService = getIt(); void initPlatformServices() { getIt.registerLazySingleton(() => p.Context()); diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 80809a23a..b06c57b04 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -3,12 +3,13 @@ import 'dart:async'; import 'package:aves/utils/android_file_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +// ignore: import_of_legacy_library_into_null_safe import 'package:streams_channel/streams_channel.dart'; abstract class StorageService { Future> getStorageVolumes(); - Future getFreeSpace(StorageVolume volume); + Future getFreeSpace(StorageVolume volume); Future> getGrantedDirectories(); @@ -25,7 +26,7 @@ abstract class StorageService { Future deleteEmptyDirectories(Iterable dirPaths); // returns media URI - Future scanFile(String path, String mimeType); + Future scanFile(String path, String mimeType); } class PlatformStorageService implements StorageService { @@ -44,16 +45,16 @@ class PlatformStorageService implements StorageService { } @override - Future getFreeSpace(StorageVolume volume) async { + Future getFreeSpace(StorageVolume volume) async { try { final result = await platform.invokeMethod('getFreeSpace', { 'path': volume.path, }); - return result as int; + return result as int?; } on PlatformException catch (e) { debugPrint('getFreeSpace failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } - return 0; + return null; } @override @@ -85,22 +86,26 @@ class PlatformStorageService implements StorageService { final result = await platform.invokeMethod('getInaccessibleDirectories', { 'dirPaths': dirPaths.toList(), }); - return (result as List).cast().map((map) => VolumeRelativeDirectory.fromMap(map)).toSet(); + if (result != null) { + return (result as List).cast().map(VolumeRelativeDirectory.fromMap).toSet(); + } } on PlatformException catch (e) { debugPrint('getInaccessibleDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } - return null; + return {}; } @override Future> getRestrictedDirectories() async { try { final result = await platform.invokeMethod('getRestrictedDirectories'); - return (result as List).cast().map((map) => VolumeRelativeDirectory.fromMap(map)).toSet(); + if (result != null) { + return (result as List).cast().map(VolumeRelativeDirectory.fromMap).toSet(); + } } on PlatformException catch (e) { debugPrint('getRestrictedDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } - return null; + return {}; } // returns whether user granted access to volume root at `volumePath` @@ -111,7 +116,7 @@ class PlatformStorageService implements StorageService { storageAccessChannel.receiveBroadcastStream({ 'path': volumePath, }).listen( - (data) => completer.complete(data as bool), + (data) => completer.complete(data as bool?), onError: completer.completeError, onDone: () { if (!completer.isCompleted) completer.complete(false); @@ -129,9 +134,10 @@ class PlatformStorageService implements StorageService { @override Future deleteEmptyDirectories(Iterable dirPaths) async { try { - return await platform.invokeMethod('deleteEmptyDirectories', { + final result = await platform.invokeMethod('deleteEmptyDirectories', { 'dirPaths': dirPaths.toList(), }); + if (result != null) return result as int; } on PlatformException catch (e) { debugPrint('deleteEmptyDirectories failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } @@ -140,14 +146,14 @@ class PlatformStorageService implements StorageService { // returns media URI @override - Future scanFile(String path, String mimeType) async { + Future scanFile(String path, String mimeType) async { debugPrint('scanFile with path=$path, mimeType=$mimeType'); try { - final uriString = await platform.invokeMethod('scanFile', { + final result = await platform.invokeMethod('scanFile', { 'path': path, 'mimeType': mimeType, }); - return Uri.tryParse(uriString ?? ''); + if (result != null) return Uri.tryParse(result); } on PlatformException catch (e) { debugPrint('scanFile failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); } diff --git a/lib/services/svg_metadata_service.dart b/lib/services/svg_metadata_service.dart index 94fbf36a4..3a282b883 100644 --- a/lib/services/svg_metadata_service.dart +++ b/lib/services/svg_metadata_service.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:aves/model/entry.dart'; import 'package:aves/services/services.dart'; import 'package:aves/utils/string_utils.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:xml/xml.dart'; @@ -15,15 +16,15 @@ class SvgMetadataService { static const _textElements = ['title', 'desc']; static const _metadataElement = 'metadata'; - static Future getSize(AvesEntry entry) async { + static Future getSize(AvesEntry entry) async { try { final data = await imageFileService.getSvg(entry.uri, entry.mimeType); final document = XmlDocument.parse(utf8.decode(data)); final root = document.rootElement; - String getAttribute(String attributeName) => root.attributes.firstWhere((a) => a.name.qualified == attributeName, orElse: () => null)?.value; - double tryParseWithoutUnit(String s) => s == null ? null : double.tryParse(s.replaceAll(RegExp(r'[a-z%]'), '')); + String? getAttribute(String attributeName) => root.attributes.firstWhereOrNull((a) => a.name.qualified == attributeName)?.value; + double? tryParseWithoutUnit(String? s) => s == null ? null : double.tryParse(s.replaceAll(RegExp(r'[a-z%]'), '')); final width = tryParseWithoutUnit(getAttribute('width')); final height = tryParseWithoutUnit(getAttribute('height')); @@ -37,7 +38,7 @@ class SvgMetadataService { if (parts.length == 4) { final vbWidth = tryParseWithoutUnit(parts[2]); final vbHeight = tryParseWithoutUnit(parts[3]); - if (vbWidth > 0 && vbHeight > 0) { + if (vbWidth != null && vbWidth > 0 && vbHeight != null && vbHeight > 0) { return Size(vbWidth, vbHeight); } } @@ -66,7 +67,7 @@ class SvgMetadataService { final docDir = Map.fromEntries([ ...root.attributes.where((a) => _attributes.contains(a.name.qualified)).map((a) => MapEntry(formatKey(a.name.qualified), a.value)), - ..._textElements.map((name) => MapEntry(formatKey(name), root.getElement(name)?.text)).where((kv) => kv.value != null), + ..._textElements.map((name) => MapEntry(formatKey(name), root.getElement(name)?.text)).where((kv) => kv.value != null).cast>(), ]); final metadata = root.getElement(_metadataElement); @@ -80,7 +81,7 @@ class SvgMetadataService { }; } catch (error, stack) { debugPrint('failed to parse XML from SVG with error=$error\n$stack'); - return null; } + return {}; } } diff --git a/lib/services/time_service.dart b/lib/services/time_service.dart index d9b284bea..af9b90613 100644 --- a/lib/services/time_service.dart +++ b/lib/services/time_service.dart @@ -2,14 +2,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; abstract class TimeService { - Future getDefaultTimeZone(); + Future getDefaultTimeZone(); } class PlatformTimeService implements TimeService { static const platform = MethodChannel('deckers.thibault/aves/time'); @override - Future getDefaultTimeZone() async { + Future getDefaultTimeZone() async { try { return await platform.invokeMethod('getDefaultTimeZone'); } on PlatformException catch (e) { diff --git a/lib/services/viewer_service.dart b/lib/services/viewer_service.dart index 76c790375..336ebd99b 100644 --- a/lib/services/viewer_service.dart +++ b/lib/services/viewer_service.dart @@ -4,10 +4,11 @@ import 'package:flutter/services.dart'; class ViewerService { static const platform = MethodChannel('deckers.thibault/aves/viewer'); - static Future getIntentData() async { + static Future> getIntentData() async { try { // returns nullable map with 'action' and possibly 'uri' 'mimeType' - return await platform.invokeMethod('getIntentData') as Map; + final result = await platform.invokeMethod('getIntentData'); + if (result != null) return (result as Map).cast(); } on PlatformException catch (e) { debugPrint('getIntentData failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index 03fde8263..1f85b0f8c 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -32,14 +32,13 @@ class Durations { static const collectionOpOverlayAnimation = Duration(milliseconds: 300); static const collectionScalingBackgroundAnimation = Duration(milliseconds: 200); static const sectionHeaderAnimation = Duration(milliseconds: 200); - static const thumbnailTransition = Duration(milliseconds: 200); static const thumbnailOverlayAnimation = Duration(milliseconds: 200); // search animations static const filterRowExpandAnimation = Duration(milliseconds: 300); // viewer animations - static const viewerVerticalPageScrollAnimation = Duration(milliseconds: 300); + static const viewerVerticalPageScrollAnimation = Duration(milliseconds: 500); static const viewerOverlayAnimation = Duration(milliseconds: 200); static const viewerOverlayChangeAnimation = Duration(milliseconds: 150); static const viewerOverlayPageScrollAnimation = Duration(milliseconds: 200); @@ -56,8 +55,10 @@ class Durations { // delays & refresh intervals static const opToastDisplay = Duration(seconds: 3); + static const opToastActionDisplay = Duration(seconds: 5); + static const infoScrollMonitoringTimerDelay = Duration(milliseconds: 100); static const collectionScrollMonitoringTimerDelay = Duration(milliseconds: 100); - static const collectionScalingCompleteNotificationDelay = Duration(milliseconds: 300); + static const highlightJumpDelay = Duration(milliseconds: 400); static const highlightScrollInitDelay = Duration(milliseconds: 800); static const videoProgressTimerInterval = Duration(milliseconds: 300); static Duration staggeredAnimationDelay = Durations.staggeredAnimation ~/ 6 * timeDilation; diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 61315ca85..d6ad028d0 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +// ignore: import_of_legacy_library_into_null_safe import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class AIcons { @@ -19,7 +20,7 @@ class AIcons { static const IconData location = Icons.place_outlined; static const IconData locationOff = Icons.location_off_outlined; static const IconData privacy = MdiIcons.shieldAccountOutline; - static const IconData raw = Icons.camera_outlined; + static const IconData raw = Icons.raw_on_outlined; static const IconData shooting = Icons.camera_outlined; static const IconData removableStorage = Icons.sd_storage_outlined; static const IconData sensorControl = Icons.explore_outlined; diff --git a/lib/theme/themes.dart b/lib/theme/themes.dart index 9cf6f14bd..9062cdf99 100644 --- a/lib/theme/themes.dart +++ b/lib/theme/themes.dart @@ -9,13 +9,13 @@ class Themes { static final darkTheme = ThemeData( brightness: Brightness.dark, accentColor: _accentColor, - scaffoldBackgroundColor: Colors.grey[900], + scaffoldBackgroundColor: Colors.grey.shade900, dialogBackgroundColor: Colors.grey[850], toggleableActiveColor: _accentColor, - tooltipTheme: TooltipThemeData( + tooltipTheme: const TooltipThemeData( verticalOffset: 32, ), - appBarTheme: AppBarTheme( + appBarTheme: const AppBarTheme( textTheme: TextTheme( headline6: TextStyle( fontSize: 20, @@ -24,15 +24,16 @@ class Themes { ), ), ), - colorScheme: ColorScheme.dark( + colorScheme: const ColorScheme.dark( primary: _accentColor, secondary: _accentColor, onPrimary: Colors.white, onSecondary: Colors.white, ), snackBarTheme: SnackBarThemeData( - backgroundColor: Colors.grey[800], - contentTextStyle: TextStyle( + backgroundColor: Colors.grey.shade800, + actionTextColor: _accentColor, + contentTextStyle: const TextStyle( color: Colors.white, ), behavior: SnackBarBehavior.floating, diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index 85fb14eca..6d2267d9e 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -2,13 +2,14 @@ import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/services.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); class AndroidFileUtils { - String primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath; + late String primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath; Set storageVolumes = {}; Set _packages = {}; List _potentialAppDirs = []; @@ -22,7 +23,7 @@ class AndroidFileUtils { Future init() async { storageVolumes = await storageService.getStorageVolumes(); // path_provider getExternalStorageDirectory() gives '/storage/emulated/0/Android/data/deckers.thibault.aves/files' - primaryStorage = storageVolumes.firstWhere((volume) => volume.isPrimary).path; + primaryStorage = storageVolumes.firstWhereOrNull((volume) => volume.isPrimary)?.path ?? '/'; dcimPath = pContext.join(primaryStorage, 'DCIM'); downloadPath = pContext.join(primaryStorage, 'Download'); moviesPath = pContext.join(primaryStorage, 'Movies'); @@ -35,16 +36,17 @@ class AndroidFileUtils { appNameChangeNotifier.notifyListeners(); } - bool isCameraPath(String path) => path != null && path.startsWith(dcimPath) && (path.endsWith('Camera') || path.endsWith('100ANDRO')); + bool isCameraPath(String path) => path.startsWith(dcimPath) && (path.endsWith('Camera') || path.endsWith('100ANDRO')); - bool isScreenshotsPath(String path) => path != null && (path.startsWith(dcimPath) || path.startsWith(picturesPath)) && path.endsWith('Screenshots'); + bool isScreenshotsPath(String path) => (path.startsWith(dcimPath) || path.startsWith(picturesPath)) && path.endsWith('Screenshots'); - bool isScreenRecordingsPath(String path) => path != null && (path.startsWith(dcimPath) || path.startsWith(moviesPath)) && (path.endsWith('Screen recordings') || path.endsWith('ScreenRecords')); + bool isScreenRecordingsPath(String path) => (path.startsWith(dcimPath) || path.startsWith(moviesPath)) && (path.endsWith('Screen recordings') || path.endsWith('ScreenRecords')); bool isDownloadPath(String path) => path == downloadPath; - StorageVolume getStorageVolume(String path) { - final volume = storageVolumes.firstWhere((v) => path.startsWith(v.path), orElse: () => null); + StorageVolume? getStorageVolume(String? path) { + if (path == null) return null; + final volume = storageVolumes.firstWhereOrNull((v) => path.startsWith(v.path)); // storage volume path includes trailing '/', but argument path may or may not, // which is an issue when the path is at the root return volume != null || path.endsWith('/') ? volume : getStorageVolume('$path/'); @@ -53,27 +55,25 @@ class AndroidFileUtils { bool isOnRemovableStorage(String path) => getStorageVolume(path)?.isRemovable ?? false; AlbumType getAlbumType(String albumPath) { - if (albumPath != null) { - if (isCameraPath(albumPath)) return AlbumType.camera; - if (isDownloadPath(albumPath)) return AlbumType.download; - if (isScreenRecordingsPath(albumPath)) return AlbumType.screenRecordings; - if (isScreenshotsPath(albumPath)) return AlbumType.screenshots; + if (isCameraPath(albumPath)) return AlbumType.camera; + if (isDownloadPath(albumPath)) return AlbumType.download; + if (isScreenRecordingsPath(albumPath)) return AlbumType.screenRecordings; + if (isScreenshotsPath(albumPath)) return AlbumType.screenshots; + + final dir = pContext.split(albumPath).last; + if (albumPath.startsWith(primaryStorage) && _potentialAppDirs.contains(dir)) return AlbumType.app; - final dir = pContext.split(albumPath).last; - if (albumPath.startsWith(primaryStorage) && _potentialAppDirs.contains(dir)) return AlbumType.app; - } return AlbumType.regular; } - String getAlbumAppPackageName(String albumPath) { - if (albumPath == null) return null; + String? getAlbumAppPackageName(String albumPath) { final dir = pContext.split(albumPath).last; - final package = _launcherPackages.firstWhere((package) => package.potentialDirs.contains(dir), orElse: () => null); + final package = _launcherPackages.firstWhereOrNull((package) => package.potentialDirs.contains(dir)); return package?.packageName; } - String getCurrentAppName(String packageName) { - final package = _packages.firstWhere((package) => package.packageName == packageName, orElse: () => null); + String? getCurrentAppName(String packageName) { + final package = _packages.firstWhereOrNull((package) => package.packageName == packageName); return package?.currentLabel; } } @@ -81,25 +81,26 @@ class AndroidFileUtils { enum AlbumType { regular, app, camera, download, screenRecordings, screenshots } class Package { - final String packageName, currentLabel, englishLabel; + final String packageName; + final String? currentLabel, englishLabel; final bool categoryLauncher, isSystem; final Set ownedDirs = {}; Package({ - this.packageName, - this.currentLabel, - this.englishLabel, - this.categoryLauncher, - this.isSystem, + required this.packageName, + required this.currentLabel, + required this.englishLabel, + required this.categoryLauncher, + required this.isSystem, }); factory Package.fromMap(Map map) { return Package( - packageName: map['packageName'], + packageName: map['packageName'] ?? '', currentLabel: map['currentLabel'], englishLabel: map['englishLabel'], - categoryLauncher: map['categoryLauncher'], - isSystem: map['isSystem'], + categoryLauncher: map['categoryLauncher'] ?? false, + isSystem: map['isSystem'] ?? false, ); } @@ -107,7 +108,7 @@ class Package { currentLabel, englishLabel, ...ownedDirs, - ].where((dir) => dir != null).toSet(); + ].where((dir) => dir != null).cast().toSet(); @override String toString() => '$runtimeType#${shortHash(this)}{packageName=$packageName, categoryLauncher=$categoryLauncher, isSystem=$isSystem, currentLabel=$currentLabel, englishLabel=$englishLabel, ownedDirs=$ownedDirs}'; @@ -115,24 +116,25 @@ class Package { @immutable class StorageVolume { - final String _description, path, state; + final String? _description; + final String path, state; final bool isPrimary, isRemovable; const StorageVolume({ - String description, - this.isPrimary, - this.isRemovable, - this.path, - this.state, + required String? description, + required this.isPrimary, + required this.isRemovable, + required this.path, + required this.state, }) : _description = description; - String getDescription(BuildContext context) { - if (_description != null) return _description; + String getDescription(BuildContext? context) { + if (_description != null) return _description!; // ideally, the context should always be provided, but in some cases (e.g. album comparison), // this would require numerous additional methods to have the context as argument // for such a minor benefit: fallback volume description on Android < N - if (isPrimary) return context?.l10n?.storageVolumeDescriptionFallbackPrimary ?? 'Internal Storage'; - return context?.l10n?.storageVolumeDescriptionFallbackNonPrimary ?? 'SD card'; + if (isPrimary) return context?.l10n.storageVolumeDescriptionFallbackPrimary ?? 'Internal Storage'; + return context?.l10n.storageVolumeDescriptionFallbackNonPrimary ?? 'SD card'; } factory StorageVolume.fromMap(Map map) { @@ -152,19 +154,19 @@ class VolumeRelativeDirectory { final String volumePath, relativeDir; const VolumeRelativeDirectory({ - this.volumePath, - this.relativeDir, + required this.volumePath, + required this.relativeDir, }); - factory VolumeRelativeDirectory.fromMap(Map map) { + static VolumeRelativeDirectory fromMap(Map map) { return VolumeRelativeDirectory( - volumePath: map['volumePath'], + volumePath: map['volumePath'] ?? '', relativeDir: map['relativeDir'] ?? '', ); } // prefer static method over a null returning factory constructor - static VolumeRelativeDirectory fromPath(String dirPath) { + static VolumeRelativeDirectory? fromPath(String dirPath) { final volume = androidFileUtils.getStorageVolume(dirPath); if (volume == null) return null; @@ -177,7 +179,7 @@ class VolumeRelativeDirectory { } String getVolumeDescription(BuildContext context) { - final volume = androidFileUtils.storageVolumes.firstWhere((volume) => volume.path == volumePath, orElse: () => null); + final volume = androidFileUtils.storageVolumes.firstWhereOrNull((volume) => volume.path == volumePath); return volume?.getDescription(context) ?? volumePath; } diff --git a/lib/utils/change_notifier.dart b/lib/utils/change_notifier.dart index 727f629ea..2a1eddca8 100644 --- a/lib/utils/change_notifier.dart +++ b/lib/utils/change_notifier.dart @@ -2,22 +2,22 @@ import 'package:flutter/foundation.dart'; // reimplemented ChangeNotifier so that it can be used anywhere, not just as a mixin class AChangeNotifier implements Listenable { - ObserverList _listeners = ObserverList(); + ObserverList? _listeners = ObserverList(); @override - void addListener(VoidCallback listener) => _listeners.add(listener); + void addListener(VoidCallback listener) => _listeners!.add(listener); @override - void removeListener(VoidCallback listener) => _listeners.remove(listener); + void removeListener(VoidCallback listener) => _listeners!.remove(listener); void dispose() => _listeners = null; void notifyListeners() { if (_listeners == null) return; - final localListeners = List.from(_listeners); + final localListeners = List.from(_listeners!); for (final listener in localListeners) { try { - if (_listeners.contains(listener)) listener(); + if (_listeners!.contains(listener)) listener(); } catch (error, stack) { debugPrint('$runtimeType failed to notify listeners with error=$error\n$stack'); } diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 731dcbec7..836a68a2b 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -15,10 +15,12 @@ class Constants { fontFeatures: [FontFeature.enable('smcp')], ); - static const embossShadow = Shadow( - color: Colors.black87, - offset: Offset(0.5, 1.0), - ); + static const embossShadows = [ + Shadow( + color: Colors.black, + offset: Offset(0.5, 1.0), + ) + ]; static const overlayUnknown = '—'; // em dash @@ -40,7 +42,7 @@ class Constants { sourceUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-main/exifinterface/exifinterface', ), Dependency( - name: 'Android-TiffBitmapFactory', + name: 'Android-TiffBitmapFactory (Aves fork)', license: 'MIT', licenseUrl: 'https://github.com/deckerst/Android-TiffBitmapFactory/blob/master/license.txt', sourceUrl: 'https://github.com/deckerst/Android-TiffBitmapFactory', @@ -67,10 +69,10 @@ class Constants { static const List flutterPlugins = [ Dependency( - name: 'Connectivity', + name: 'Connectivity Plus', license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/connectivity/connectivity/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/connectivity/connectivity', + licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/blob/main/packages/connectivity_plus/LICENSE', + sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/connectivity_plus', ), Dependency( name: 'FlutterFire (Core, Analytics, Crashlytics)', @@ -79,7 +81,7 @@ class Constants { sourceUrl: 'https://github.com/FirebaseExtended/flutterfire', ), Dependency( - name: 'fijkplayer', + name: 'fijkplayer (Aves fork)', license: 'MIT', licenseUrl: 'https://github.com/deckerst/fijkplayer/blob/master/LICENSE', sourceUrl: 'https://github.com/deckerst/fijkplayer', @@ -97,10 +99,10 @@ class Constants { sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter', ), Dependency( - name: 'Package Info', + name: 'Package Info Plus', license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/package_info/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/package_info', + licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/package_info_plus/LICENSE', + sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/package_info_plus', ), Dependency( name: 'Permission Handler', @@ -127,10 +129,10 @@ class Constants { sourceUrl: 'https://github.com/tekartik/sqflite', ), Dependency( - name: 'Streams Channel', + name: 'Streams Channel (Aves fork)', license: 'Apache 2.0', - licenseUrl: 'https://github.com/loup-v/streams_channel/blob/master/LICENSE', - sourceUrl: 'https://github.com/loup-v/streams_channel', + licenseUrl: 'https://github.com/deckerst/aves_streams_channel/blob/master/LICENSE', + sourceUrl: 'https://github.com/deckerst/aves_streams_channel', ), Dependency( name: 'URL Launcher', @@ -229,7 +231,7 @@ class Constants { sourceUrl: 'https://github.com/benPesso/flutter_decorated_icon', ), Dependency( - name: 'Expansion Tile Card', + name: 'Expansion Tile Card (Aves fork)', license: 'BSD 3-Clause', licenseUrl: 'https://github.com/deckerst/expansion_tile_card/blob/master/LICENSE', sourceUrl: 'https://github.com/deckerst/expansion_tile_card', @@ -249,8 +251,8 @@ class Constants { Dependency( name: 'Flutter Markdown', license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/flutter/flutter_markdown/blob/master/LICENSE', - sourceUrl: 'https://github.com/flutter/flutter_markdown', + licenseUrl: 'https://github.com/flutter/packages/blob/master/packages/flutter_markdown/LICENSE', + sourceUrl: 'https://github.com/flutter/packages/tree/master/packages/flutter_markdown', ), Dependency( name: 'Flutter Staggered Animations', @@ -310,9 +312,9 @@ class Dependency { final String licenseUrl; const Dependency({ - @required this.name, - @required this.license, - @required this.licenseUrl, - @required this.sourceUrl, + required this.name, + required this.license, + required this.licenseUrl, + required this.sourceUrl, }); } diff --git a/lib/utils/debouncer.dart b/lib/utils/debouncer.dart index b41cd90bc..0fc03fc88 100644 --- a/lib/utils/debouncer.dart +++ b/lib/utils/debouncer.dart @@ -5,11 +5,11 @@ import 'package:flutter/foundation.dart'; class Debouncer { final Duration delay; - Timer _timer; + Timer? _timer; - Debouncer({@required this.delay}); + Debouncer({required this.delay}); - void call(Function action) { + void call(VoidCallback action) { _timer?.cancel(); _timer = Timer(delay, action); } diff --git a/lib/utils/math_utils.dart b/lib/utils/math_utils.dart index 541ebb5c5..33c49fe99 100644 --- a/lib/utils/math_utils.dart +++ b/lib/utils/math_utils.dart @@ -1,7 +1,5 @@ import 'dart:math'; -import 'package:flutter/foundation.dart'; - final double _log2 = log(2); const double _piOver180 = pi / 180.0; @@ -9,12 +7,12 @@ double toDegrees(num radians) => radians / _piOver180; double toRadians(num degrees) => degrees * _piOver180; -int highestPowerOf2(num x) => x < 1 ? 0 : pow(2, (log(x) / _log2).floor()); +int highestPowerOf2(num x) => x < 1 ? 0 : pow(2, (log(x) / _log2).floor()) as int; -double roundToPrecision(final double value, {@required final int decimals}) => (value * pow(10, decimals)).round() / pow(10, decimals); +double roundToPrecision(final double value, {required final int decimals}) => (value * pow(10, decimals)).round() / pow(10, decimals); // e.g. x=12345, precision=3 should return 13000 int ceilBy(num x, int precision) { final factor = pow(10, precision); - return (x / factor).ceil() * factor; + return (x / factor).ceil() * (factor as int); } diff --git a/lib/utils/string_utils.dart b/lib/utils/string_utils.dart index d1d747aad..4b4950e41 100644 --- a/lib/utils/string_utils.dart +++ b/lib/utils/string_utils.dart @@ -3,7 +3,7 @@ extension ExtraString on String { static final _sentenceCaseStep2 = RegExp(r'([a-z])([A-Z])'); String toSentenceCase() { - var s = replaceFirstMapped(RegExp('.'), (m) => m.group(0).toUpperCase()); + var s = replaceFirstMapped(RegExp('.'), (m) => m.group(0)!.toUpperCase()); return s.replaceAllMapped(_sentenceCaseStep1, (m) => ' ${m.group(1)}').replaceAllMapped(_sentenceCaseStep2, (m) => '${m.group(1)} ${m.group(2)}').trim(); } } diff --git a/lib/utils/time_utils.dart b/lib/utils/time_utils.dart index 9e95fea75..4769b2557 100644 --- a/lib/utils/time_utils.dart +++ b/lib/utils/time_utils.dart @@ -15,15 +15,15 @@ String formatPreciseDuration(Duration d) { } extension ExtraDateTime on DateTime { - bool isAtSameYearAs(DateTime other) => this?.year == other?.year; + bool isAtSameYearAs(DateTime? other) => year == other?.year; - bool isAtSameMonthAs(DateTime other) => isAtSameYearAs(other) && this?.month == other?.month; + bool isAtSameMonthAs(DateTime? other) => isAtSameYearAs(other) && month == other?.month; - bool isAtSameDayAs(DateTime other) => isAtSameMonthAs(other) && this?.day == other?.day; + bool isAtSameDayAs(DateTime? other) => isAtSameMonthAs(other) && day == other?.day; bool get isToday => isAtSameDayAs(DateTime.now()); - bool get isYesterday => isAtSameDayAs(DateTime.now().subtract(Duration(days: 1))); + bool get isYesterday => isAtSameDayAs(DateTime.now().subtract(const Duration(days: 1))); bool get isThisMonth => isAtSameMonthAs(DateTime.now()); diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart index d368bf332..31f2a3091 100644 --- a/lib/widgets/about/about_page.dart +++ b/lib/widgets/about/about_page.dart @@ -18,15 +18,15 @@ class AboutPage extends StatelessWidget { child: CustomScrollView( slivers: [ SliverPadding( - padding: EdgeInsets.only(top: 16), + padding: const EdgeInsets.only(top: 16), sliver: SliverList( delegate: SliverChildListDelegate( [ AppReference(), - Divider(), + const Divider(), AboutUpdate(), AboutCredits(), - Divider(), + const Divider(), ], ), ), diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart index 0e383a192..8d309ee5e 100644 --- a/lib/widgets/about/app_ref.dart +++ b/lib/widgets/about/app_ref.dart @@ -5,7 +5,7 @@ import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:flutter/material.dart'; -import 'package:package_info/package_info.dart'; +import 'package:package_info_plus/package_info_plus.dart'; class AppReference extends StatefulWidget { @override @@ -13,7 +13,7 @@ class AppReference extends StatefulWidget { } class _AppReferenceState extends State { - Future _packageInfoLoader; + late Future _packageInfoLoader; @override void initState() { @@ -28,14 +28,14 @@ class _AppReferenceState extends State { children: [ _buildAvesLine(), _buildFlutterLine(), - SizedBox(height: 16), + const SizedBox(height: 16), ], ), ); } Widget _buildAvesLine() { - final style = TextStyle( + const style = TextStyle( fontSize: 20, fontWeight: FontWeight.normal, letterSpacing: 1.0, @@ -47,7 +47,7 @@ class _AppReferenceState extends State { builder: (context, snapshot) { return LinkChip( leading: AvesLogo( - size: style.fontSize * MediaQuery.textScaleFactorOf(context) * 1.25, + size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.25, ), text: '${context.l10n.appName} ${snapshot.data?.version}', url: 'https://github.com/deckerst/aves', @@ -59,16 +59,16 @@ class _AppReferenceState extends State { Widget _buildFlutterLine() { final style = DefaultTextStyle.of(context).style; - final subColor = style.color.withOpacity(.6); + final subColor = style.color!.withOpacity(.6); return Text.rich( TextSpan( children: [ WidgetSpan( child: Padding( - padding: EdgeInsetsDirectional.only(end: 4), + padding: const EdgeInsetsDirectional.only(end: 4), child: FlutterLogo( - size: style.fontSize * 1.25, + size: style.fontSize! * 1.25, ), ), ), diff --git a/lib/widgets/about/credits.dart b/lib/widgets/about/credits.dart index 8cf4f1a5e..ce081b1cf 100644 --- a/lib/widgets/about/credits.dart +++ b/lib/widgets/about/credits.dart @@ -9,12 +9,12 @@ class AboutCredits extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ConstrainedBox( - constraints: BoxConstraints(minHeight: 48), + constraints: const BoxConstraints(minHeight: 48), child: Align( alignment: AlignmentDirectional.centerStart, child: Text(context.l10n.aboutCredits, style: Constants.titleTextStyle), @@ -24,7 +24,7 @@ class AboutCredits extends StatelessWidget { TextSpan( children: [ TextSpan(text: context.l10n.aboutCreditsWorldAtlas1), - WidgetSpan( + const WidgetSpan( child: LinkChip( text: 'World Atlas', url: 'https://github.com/topojson/world-atlas', @@ -36,7 +36,7 @@ class AboutCredits extends StatelessWidget { ], ), ), - SizedBox(height: 16), + const SizedBox(height: 16), ], ), ); diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index ff1f2a04f..55689796f 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -12,8 +12,8 @@ class Licenses extends StatefulWidget { } class _LicensesState extends State { - final ValueNotifier _expandedNotifier = ValueNotifier(null); - List _platform, _flutterPlugins, _flutterPackages, _dartPackages; + final ValueNotifier _expandedNotifier = ValueNotifier(null); + late List _platform, _flutterPlugins, _flutterPackages, _dartPackages; @override void initState() { @@ -36,12 +36,12 @@ class _LicensesState extends State { @override Widget build(BuildContext context) { return SliverPadding( - padding: EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric(horizontal: 8), sliver: SliverList( delegate: SliverChildListDelegate( [ _buildHeader(), - SizedBox(height: 16), + const SizedBox(height: 16), AvesExpansionTile( title: context.l10n.aboutLicensesAndroidLibraries, color: BrandColors.android, @@ -76,7 +76,7 @@ class _LicensesState extends State { // as of Flutter v1.22.4, `cardColor` is used as a background color by `LicensePage` cardColor: Theme.of(context).scaffoldBackgroundColor, ), - child: LicensePage(), + child: const LicensePage(), ), ), ), @@ -91,18 +91,18 @@ class _LicensesState extends State { Widget _buildHeader() { return Padding( - padding: EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric(horizontal: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ConstrainedBox( - constraints: BoxConstraints(minHeight: 48), + constraints: const BoxConstraints(minHeight: 48), child: Align( alignment: AlignmentDirectional.centerStart, child: Text(context.l10n.aboutLicenses, style: Constants.titleTextStyle), ), ), - SizedBox(height: 8), + const SizedBox(height: 8), Text(context.l10n.aboutLicensesBanner), ], ), @@ -118,21 +118,21 @@ class LicenseRow extends StatelessWidget { @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; - final bodyTextStyle = textTheme.bodyText2; - final subColor = bodyTextStyle.color.withOpacity(.6); + final bodyTextStyle = textTheme.bodyText2!; + final subColor = bodyTextStyle.color!.withOpacity(.6); return Padding( - padding: EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ LinkChip( text: package.name, url: package.sourceUrl, - textStyle: TextStyle(fontWeight: FontWeight.bold), + textStyle: const TextStyle(fontWeight: FontWeight.bold), ), Padding( - padding: EdgeInsetsDirectional.only(start: 16), + padding: const EdgeInsetsDirectional.only(start: 16), child: LinkChip( text: package.license, url: package.licenseUrl, diff --git a/lib/widgets/about/news_badge.dart b/lib/widgets/about/news_badge.dart index 69a571dde..7ec8d6c1a 100644 --- a/lib/widgets/about/news_badge.dart +++ b/lib/widgets/about/news_badge.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; class AboutNewsBadge extends StatelessWidget { + const AboutNewsBadge(); + @override Widget build(BuildContext context) { - return Icon( + return const Icon( Icons.circle, size: 12, color: Colors.red, diff --git a/lib/widgets/about/update.dart b/lib/widgets/about/update.dart index 02248105d..5b76768f4 100644 --- a/lib/widgets/about/update.dart +++ b/lib/widgets/about/update.dart @@ -11,7 +11,7 @@ class AboutUpdate extends StatefulWidget { } class _AboutUpdateState extends State { - Future _updateChecker; + late Future _updateChecker; @override void initState() { @@ -25,22 +25,22 @@ class _AboutUpdateState extends State { future: _updateChecker, builder: (context, snapshot) { final newVersionAvailable = snapshot.data == true; - if (!newVersionAvailable) return SizedBox(); + if (!newVersionAvailable) return const SizedBox(); return Column( children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ConstrainedBox( - constraints: BoxConstraints(minHeight: 48), + constraints: const BoxConstraints(minHeight: 48), child: Align( alignment: AlignmentDirectional.centerStart, child: Text.rich( TextSpan( children: [ - WidgetSpan( + const WidgetSpan( child: Padding( padding: EdgeInsetsDirectional.only(end: 8), child: AboutNewsBadge(), @@ -61,7 +61,7 @@ class _AboutUpdateState extends State { child: LinkChip( text: context.l10n.aboutUpdateGithub, url: 'https://github.com/deckerst/aves/releases', - textStyle: TextStyle(fontWeight: FontWeight.bold), + textStyle: const TextStyle(fontWeight: FontWeight.bold), ), alignment: PlaceholderAlignment.middle, ), @@ -70,7 +70,7 @@ class _AboutUpdateState extends State { child: LinkChip( text: context.l10n.aboutUpdateGooglePlay, url: 'https://play.google.com/store/apps/details?id=deckers.thibault.aves', - textStyle: TextStyle(fontWeight: FontWeight.bold), + textStyle: const TextStyle(fontWeight: FontWeight.bold), ), alignment: PlaceholderAlignment.middle, ), @@ -78,11 +78,11 @@ class _AboutUpdateState extends State { ], ), ), - SizedBox(height: 16), + const SizedBox(height: 16), ], ), ), - Divider(), + const Divider(), ], ); }, diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart new file mode 100644 index 000000000..df8d6b4ce --- /dev/null +++ b/lib/widgets/aves_app.dart @@ -0,0 +1,173 @@ +import 'dart:ui'; + +import 'package:aves/app_mode.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/media_store_source.dart'; +import 'package:aves/services/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/behaviour/route_tracker.dart'; +import 'package:aves/widgets/common/behaviour/routes.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; +import 'package:aves/widgets/home_page.dart'; +import 'package:aves/widgets/welcome_page.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_analytics/observer.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:overlay_support/overlay_support.dart'; +import 'package:provider/provider.dart'; + +class AvesApp extends StatefulWidget { + @override + _AvesAppState createState() => _AvesAppState(); +} + +class _AvesAppState extends State { + final ValueNotifier appModeNotifier = ValueNotifier(AppMode.main); + late Future _appSetup; + final _mediaStoreSource = MediaStoreSource(); + final Debouncer _contentChangeDebouncer = Debouncer(delay: Durations.contentChangeDebounceDelay); + final Set changedUris = {}; + + // observers are not registered when using the same list object with different items + // the list itself needs to be reassigned + List _navigatorObservers = []; + final EventChannel _contentChangeChannel = const EventChannel('deckers.thibault/aves/contentchange'); + final EventChannel _newIntentChannel = const EventChannel('deckers.thibault/aves/intent'); + final GlobalKey _navigatorKey = GlobalKey(debugLabel: 'app-navigator'); + + Widget getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage(); + + @override + void initState() { + super.initState(); + initPlatformServices(); + _appSetup = _setup(); + _contentChangeChannel.receiveBroadcastStream().listen((event) => _onContentChange(event as String?)); + _newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?)); + } + + @override + Widget build(BuildContext context) { + // place the settings provider above `MaterialApp` + // so it can be used during navigation transitions + return ChangeNotifierProvider.value( + value: settings, + child: ListenableProvider>.value( + value: appModeNotifier, + child: Provider.value( + value: _mediaStoreSource, + child: HighlightInfoProvider( + child: OverlaySupport( + child: FutureBuilder( + future: _appSetup, + builder: (context, snapshot) { + final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done; + final home = initialized + ? getFirstPage() + : Scaffold( + body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(), + ); + return Selector( + selector: (context, s) => s.locale, + builder: (context, settingsLocale, child) { + return MaterialApp( + navigatorKey: _navigatorKey, + home: home, + navigatorObservers: _navigatorObservers, + onGenerateTitle: (context) => context.l10n.appName, + darkTheme: Themes.darkTheme, + themeMode: ThemeMode.dark, + locale: settingsLocale, + localizationsDelegates: [ + ...AppLocalizations.localizationsDelegates, + ], + supportedLocales: AppLocalizations.supportedLocales, + // checkerboardRasterCacheImages: true, + // checkerboardOffscreenLayers: true, + ); + }); + }, + ), + ), + ), + ), + ), + ); + } + + Widget _buildError(Object error) { + return Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(AIcons.error), + const SizedBox(height: 16), + Text(error.toString()), + ], + ), + ); + } + + Future _setup() async { + await Firebase.initializeApp().then((app) { + final crashlytics = FirebaseCrashlytics.instance; + FlutterError.onError = crashlytics.recordFlutterError; + crashlytics.setCustomKey('locales', window.locales.join(', ')); + final now = DateTime.now(); + crashlytics.setCustomKey('timezone', '${now.timeZoneName} (${now.timeZoneOffset})'); + crashlytics.setCustomKey( + 'build_mode', + kReleaseMode + ? 'release' + : kProfileMode + ? 'profile' + : 'debug'); + }); + await settings.init(); + await settings.initFirebase(); + _navigatorObservers = [ + FirebaseAnalyticsObserver(analytics: FirebaseAnalytics()), + CrashlyticsRouteTracker(), + ]; + } + + void _onNewIntent(Map? intentData) { + debugPrint('$runtimeType onNewIntent with intentData=$intentData'); + + // do not reset when relaunching the app + if (appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) return; + + FirebaseCrashlytics.instance.log('New intent'); + _navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute( + settings: const RouteSettings(name: HomePage.routeName), + builder: (_) => getFirstPage(intentData: intentData), + )); + } + + void _onContentChange(String? uri) { + if (uri != null) changedUris.add(uri); + if (changedUris.isNotEmpty) { + _contentChangeDebouncer(() async { + final todo = changedUris.toSet(); + changedUris.clear(); + final tempUris = await _mediaStoreSource.refreshUris(todo); + if (tempUris.isNotEmpty) { + changedUris.addAll(tempUris); + _onContentChange(null); + } + }); + } + } +} diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 6fb8685ad..f9d3e38cd 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -35,9 +35,9 @@ class CollectionAppBar extends StatefulWidget { final CollectionLens collection; const CollectionAppBar({ - Key key, - @required this.appBarHeightNotifier, - @required this.collection, + Key? key, + required this.appBarHeightNotifier, + required this.collection, }) : super(key: key); @override @@ -46,9 +46,9 @@ class CollectionAppBar extends StatefulWidget { class _CollectionAppBarState extends State with SingleTickerProviderStateMixin { final TextEditingController _searchFieldController = TextEditingController(); - EntrySetActionDelegate _actionDelegate; - AnimationController _browseToSelectAnimation; - Future _canAddShortcutsLoader; + final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate(); + late AnimationController _browseToSelectAnimation; + late Future _canAddShortcutsLoader; CollectionLens get collection => widget.collection; @@ -59,16 +59,13 @@ class _CollectionAppBarState extends State with SingleTickerPr @override void initState() { super.initState(); - _actionDelegate = EntrySetActionDelegate( - collection: collection, - ); _browseToSelectAnimation = AnimationController( duration: Durations.iconAnimation, vsync: this, ); _canAddShortcutsLoader = AppShortcutService.canPin(); _registerWidget(widget); - WidgetsBinding.instance.addPostFrameCallback((_) => _updateHeight()); + WidgetsBinding.instance!.addPostFrameCallback((_) => _updateHeight()); } @override @@ -127,8 +124,8 @@ class _CollectionAppBarState extends State with SingleTickerPr } Widget _buildAppBarLeading() { - VoidCallback onPressed; - String tooltip; + VoidCallback? onPressed; + String? tooltip; if (collection.isBrowsing) { onPressed = Scaffold.of(context).openDrawer; tooltip = MaterialLocalizations.of(context).openAppDrawerTooltip; @@ -137,7 +134,7 @@ class _CollectionAppBarState extends State with SingleTickerPr tooltip = MaterialLocalizations.of(context).backButtonTooltip; } return IconButton( - key: Key('appbar-leading-button'), + key: const Key('appbar-leading-button'), icon: AnimatedIcon( icon: AnimatedIcons.menu_arrow, progress: _browseToSelectAnimation, @@ -147,7 +144,7 @@ class _CollectionAppBarState extends State with SingleTickerPr ); } - Widget _buildAppBarTitle() { + Widget? _buildAppBarTitle() { if (collection.isBrowsing) { final appMode = context.watch>().value; Widget title = Text(appMode.isPicking ? context.l10n.collectionPickPageTitle : context.l10n.collectionPageTitle); @@ -197,19 +194,19 @@ class _CollectionAppBarState extends State with SingleTickerPr builder: (context, snapshot) { final canAddShortcuts = snapshot.data ?? false; return PopupMenuButton( - key: Key('appbar-menu-button'), + key: const Key('appbar-menu-button'), itemBuilder: (context) { final isNotEmpty = !collection.isEmpty; final hasSelection = collection.selection.isNotEmpty; return [ PopupMenuItem( - key: Key('menu-sort'), + key: const Key('menu-sort'), value: CollectionAction.sort, child: MenuRow(text: context.l10n.menuActionSort, icon: AIcons.sort), ), if (collection.sortFactor == EntrySortFactor.date) PopupMenuItem( - key: Key('menu-group'), + key: const Key('menu-group'), value: CollectionAction.group, child: MenuRow(text: context.l10n.menuActionGroup, icon: AIcons.group), ), @@ -231,7 +228,7 @@ class _CollectionAppBarState extends State with SingleTickerPr ), ], if (collection.isSelecting) ...[ - PopupMenuDivider(), + const PopupMenuDivider(), PopupMenuItem( value: CollectionAction.copy, enabled: hasSelection, @@ -247,7 +244,7 @@ class _CollectionAppBarState extends State with SingleTickerPr enabled: hasSelection, child: MenuRow(text: context.l10n.collectionActionRefreshMetadata), ), - PopupMenuDivider(), + const PopupMenuDivider(), PopupMenuItem( value: CollectionAction.selectAll, enabled: collection.selection.length < collection.entryCount, @@ -359,17 +356,18 @@ class _CollectionAppBarState extends State with SingleTickerPr final sortedFilters = List.from(filters)..sort(); defaultName = sortedFilters.first.getLabel(context); } - final result = await showDialog>( + final result = await showDialog>( context: context, builder: (context) => AddShortcutDialog( collection: collection, - defaultName: defaultName, + defaultName: defaultName ?? '', ), ); + if (result == null) return; + final coverEntry = result.item1; final name = result.item2; - - if (name == null || name.isEmpty) return; + if (name.isEmpty) return; unawaited(AppShortcutService.pin(name, coverEntry, filters)); } @@ -389,7 +387,7 @@ class _CollectionAppBarState extends State with SingleTickerPr Navigator.push( context, MaterialPageRoute( - settings: RouteSettings(name: StatsPage.routeName), + settings: const RouteSettings(name: StatsPage.routeName), builder: (context) => StatsPage( source: source, parentCollection: collection, diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index fba492c3a..e53d4b09b 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -4,7 +4,6 @@ import 'package:aves/app_mode.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; -import 'package:aves/model/highlight.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/ref/mime_types.dart'; @@ -22,7 +21,7 @@ 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/section_layout.dart'; +import 'package:aves/widgets/common/grid/item_tracker.dart'; import 'package:aves/widgets/common/grid/sliver.dart'; import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart'; @@ -35,29 +34,30 @@ import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class CollectionGrid extends StatefulWidget { - final String settingsRouteKey; + final String? settingsRouteKey; const CollectionGrid({ + Key? key, this.settingsRouteKey, - }); + }) : super(key: key); @override _CollectionGridState createState() => _CollectionGridState(); } class _CollectionGridState extends State { - TileExtentController _tileExtentController; + TileExtentController? _tileExtentController; @override Widget build(BuildContext context) { _tileExtentController ??= TileExtentController( - settingsRouteKey: widget.settingsRouteKey ?? context.currentRouteName, + settingsRouteKey: widget.settingsRouteKey ?? context.currentRouteName!, columnCountDefault: 4, extentMin: 46, - spacing: 0, + spacing: 2, ); return TileExtentControllerProvider( - controller: _tileExtentController, + controller: _tileExtentController!, child: _CollectionGridContent(), ); } @@ -75,19 +75,21 @@ class _CollectionGridContent extends StatelessWidget { builder: (context, tileExtent, child) { return ThumbnailTheme( extent: tileExtent, - child: Selector>( - selector: (context, c) => Tuple2(c.viewportSize.width, c.columnCount), + child: Selector>( + selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing), builder: (context, c, child) { final scrollableWidth = c.item1; final columnCount = c.item2; + final tileSpacing = c.item3; // do not listen for animation delay change final controller = Provider.of(context, listen: false); final tileAnimationDelay = controller.getTileAnimationDelay(Durations.staggeredAnimationPageTarget); return SectionedEntryListLayoutProvider( collection: collection, scrollableWidth: scrollableWidth, - tileExtent: tileExtent, columnCount: columnCount, + spacing: tileSpacing, + tileExtent: tileExtent, tileBuilder: (entry) => InteractiveThumbnail( key: ValueKey(entry.contentId), collection: collection, @@ -99,7 +101,7 @@ class _CollectionGridContent extends StatelessWidget { child: _CollectionSectionedContent( collection: collection, isScrollingNotifier: _isScrollingNotifier, - scrollController: PrimaryScrollController.of(context), + scrollController: PrimaryScrollController.of(context)!, ), ); }, @@ -119,42 +121,46 @@ class _CollectionSectionedContent extends StatefulWidget { final ScrollController scrollController; const _CollectionSectionedContent({ - @required this.collection, - @required this.isScrollingNotifier, - @required this.scrollController, + required this.collection, + required this.isScrollingNotifier, + required this.scrollController, }); @override _CollectionSectionedContentState createState() => _CollectionSectionedContentState(); } -class _CollectionSectionedContentState extends State<_CollectionSectionedContent> { +class _CollectionSectionedContentState extends State<_CollectionSectionedContent> with WidgetsBindingObserver, GridItemTrackerMixin { CollectionLens get collection => widget.collection; + @override ScrollController get scrollController => widget.scrollController; - final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); - final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable'); + @override + final ValueNotifier appBarHeightNotifier = ValueNotifier(0); + + @override + final GlobalKey scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable'); @override Widget build(BuildContext context) { final scrollView = AnimationLimiter( child: _CollectionScrollView( - scrollableKey: _scrollableKey, + scrollableKey: scrollableKey, collection: collection, appBar: CollectionAppBar( - appBarHeightNotifier: _appBarHeightNotifier, + appBarHeightNotifier: appBarHeightNotifier, collection: collection, ), - appBarHeightNotifier: _appBarHeightNotifier, + appBarHeightNotifier: appBarHeightNotifier, isScrollingNotifier: widget.isScrollingNotifier, scrollController: scrollController, ), ); final scaler = _CollectionScaler( - scrollableKey: _scrollableKey, - appBarHeightNotifier: _appBarHeightNotifier, + scrollableKey: scrollableKey, + appBarHeightNotifier: appBarHeightNotifier, child: scrollView, ); @@ -163,7 +169,7 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent selectable: isMainMode, collection: collection, scrollController: scrollController, - appBarHeightNotifier: _appBarHeightNotifier, + appBarHeightNotifier: appBarHeightNotifier, child: scaler, ); @@ -177,9 +183,9 @@ class _CollectionScaler extends StatelessWidget { final Widget child; const _CollectionScaler({ - @required this.scrollableKey, - @required this.appBarHeightNotifier, - @required this.child, + required this.scrollableKey, + required this.appBarHeightNotifier, + required this.child, }); @override @@ -187,15 +193,13 @@ class _CollectionScaler extends StatelessWidget { final tileSpacing = context.select((controller) => controller.spacing); return GridScaleGestureDetector( scrollableKey: scrollableKey, - appBarHeightNotifier: appBarHeightNotifier, gridBuilder: (center, extent, child) => CustomPaint( - // painting the thumbnail half-border on top of the grid yields artifacts, - // so we use a `foregroundPainter` to cover them instead - foregroundPainter: GridPainter( + painter: GridPainter( center: center, extent: extent, spacing: tileSpacing, - strokeWidth: DecoratedThumbnail.borderWidth * 2, + borderWidth: DecoratedThumbnail.borderWidth, + borderRadius: Radius.zero, color: DecoratedThumbnail.borderColor, ), child: child, @@ -204,16 +208,11 @@ class _CollectionScaler extends StatelessWidget { extent: extent, child: DecoratedThumbnail( entry: entry, - extent: extent, + tileExtent: context.read().effectiveExtentMax, selectable: false, highlightable: false, ), ), - getScaledItemTileRect: (context, entry) { - final sectionedListLayout = context.read>(); - return sectionedListLayout.getTileRect(entry) ?? Rect.zero; - }, - onScaled: (entry) => context.read().set(entry), child: child, ); } @@ -228,12 +227,12 @@ class _CollectionScrollView extends StatefulWidget { final ScrollController scrollController; const _CollectionScrollView({ - @required this.scrollableKey, - @required this.collection, - @required this.appBar, - @required this.appBarHeightNotifier, - @required this.isScrollingNotifier, - @required this.scrollController, + required this.scrollableKey, + required this.collection, + required this.appBar, + required this.appBarHeightNotifier, + required this.isScrollingNotifier, + required this.scrollController, }); @override @@ -241,7 +240,7 @@ class _CollectionScrollView extends StatefulWidget { } class _CollectionScrollViewState extends State<_CollectionScrollView> { - Timer _scrollMonitoringTimer; + Timer? _scrollMonitoringTimer; @override void initState() { @@ -316,8 +315,8 @@ 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 ? NeverScrollableScrollPhysics() : SloppyScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - cacheExtent: context.select((controller) => controller.effectiveExtentMax * 2), + physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : const SloppyScrollPhysics(parent: AlwaysScrollableScrollPhysics()), + cacheExtent: context.select((controller) => controller.effectiveExtentMax), slivers: [ appBar, collection.isEmpty @@ -336,7 +335,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> { valueListenable: collection.source.stateNotifier, builder: (context, sourceState, child) { if (sourceState == SourceState.loading) { - return SizedBox.shrink(); + return const SizedBox.shrink(); } if (collection.filters.any((filter) => filter is FavouriteFilter)) { return EmptyContent( diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 6c8704dc7..5bde8c65f 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -46,7 +46,9 @@ class _CollectionPageState extends State { bottom: false, child: ChangeNotifierProvider.value( value: collection, - child: CollectionGrid(), + child: const CollectionGrid( + key: Key('collection-grid'), + ), ), ), ), diff --git a/lib/widgets/collection/draggable_thumb_label.dart b/lib/widgets/collection/draggable_thumb_label.dart index 8706e10be..d7776d9a2 100644 --- a/lib/widgets/collection/draggable_thumb_label.dart +++ b/lib/widgets/collection/draggable_thumb_label.dart @@ -13,8 +13,8 @@ class CollectionDraggableThumbLabel extends StatelessWidget { final double offsetY; const CollectionDraggableThumbLabel({ - @required this.collection, - @required this.offsetY, + required this.collection, + required this.offsetY, }); @override @@ -28,7 +28,7 @@ class CollectionDraggableThumbLabel extends StatelessWidget { case EntryGroupFactor.album: return [ DraggableThumbLabel.formatMonthThumbLabel(context, entry.bestDate), - if (_hasMultipleSections(context)) context.read().getAlbumDisplayName(context, entry.directory), + if (_showAlbumName(context, entry)) _getAlbumName(context, entry), ]; case EntryGroupFactor.month: case EntryGroupFactor.none: @@ -40,21 +40,23 @@ class CollectionDraggableThumbLabel extends StatelessWidget { DraggableThumbLabel.formatDayThumbLabel(context, entry.bestDate), ]; } - break; case EntrySortFactor.name: return [ - if (_hasMultipleSections(context)) context.read().getAlbumDisplayName(context, entry.directory), - entry.bestTitle, + if (_showAlbumName(context, entry)) _getAlbumName(context, entry), + if (entry.bestTitle != null) entry.bestTitle!, ]; case EntrySortFactor.size: return [ - formatFilesize(entry.sizeBytes, round: 0), + if (entry.sizeBytes != null) formatFilesize(entry.sizeBytes!, round: 0), ]; } - return []; }, ); } bool _hasMultipleSections(BuildContext context) => context.read>().sections.length > 1; + + bool _showAlbumName(BuildContext context, AvesEntry entry) => _hasMultipleSections(context) && entry.directory != null; + + String _getAlbumName(BuildContext context, AvesEntry entry) => context.read().getAlbumDisplayName(context, entry.directory!); } diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 8b2e3094f..57a51551c 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -3,41 +3,36 @@ import 'dart:async'; import 'package:aves/model/actions/collection_actions.dart'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/move_type.dart'; -import 'package:aves/model/entry.dart'; +import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/highlight.dart'; import 'package:aves/model/source/collection_lens.dart'; -import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_op_events.dart'; import 'package:aves/services/services.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.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'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/filter_grids/album_pick.dart'; -import 'package:flutter/foundation.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:pedantic/pedantic.dart'; +import 'package:provider/provider.dart'; class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { - final CollectionLens collection; - - CollectionSource get source => collection.source; - - Set get selection => collection.selection; - - EntrySetActionDelegate({ - @required this.collection, - }); - void onEntryActionSelected(BuildContext context, EntryAction action) { switch (action) { case EntryAction.delete: _showDeleteDialog(context); break; case EntryAction.share: - AndroidAppService.shareEntries(selection).then((success) { + final collection = context.read(); + AndroidAppService.shareEntries(collection.selection).then((success) { if (!success) showNoMatchingAppDialog(context); }); break; @@ -55,16 +50,25 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware _moveSelection(context, moveType: MoveType.move); break; case CollectionAction.refreshMetadata: - source.refreshMetadata(selection); - collection.browse(); + _refreshMetadata(context); break; default: break; } } - Future _moveSelection(BuildContext context, {@required MoveType moveType}) async { - final selectionDirs = selection.where((e) => e.path != null).map((e) => e.directory).toSet(); + void _refreshMetadata(BuildContext context) { + final collection = context.read(); + collection.source.refreshMetadata(collection.selection); + collection.browse(); + } + + Future _moveSelection(BuildContext context, {required MoveType moveType}) async { + final collection = context.read(); + final source = collection.source; + final selection = collection.selection; + + final selectionDirs = selection.where((e) => e.path != null).map((e) => e.directory).cast().toSet(); if (moveType == MoveType.move) { // check whether moving is possible given OS restrictions, // before asking to pick a destination album @@ -82,7 +86,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware final destinationAlbum = await Navigator.push( context, MaterialPageRoute( - settings: RouteSettings(name: AlbumPickPage.routeName), + settings: const RouteSettings(name: AlbumPickPage.routeName), builder: (context) => AlbumPickPage(source: source, moveType: moveType), ), ); @@ -99,6 +103,8 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware final copy = moveType == MoveType.copy; final todoCount = todoEntries.length; + assert(todoCount > 0); + source.pauseMonitoring(); showOpReport( context: context, @@ -115,6 +121,11 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware collection.browse(); source.resumeMonitoring(); + // cleanup + if (moveType == MoveType.move) { + await storageService.deleteEmptyDirectories(selectionDirs); + } + final l10n = context.l10n; final movedCount = movedOps.length; if (movedCount < todoCount) { @@ -122,19 +133,56 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware showFeedback(context, copy ? l10n.collectionCopyFailureFeedback(count) : l10n.collectionMoveFailureFeedback(count)); } else { final count = movedCount; - showFeedback(context, copy ? l10n.collectionCopySuccessFeedback(count) : l10n.collectionMoveSuccessFeedback(count)); - } - - // cleanup - if (moveType == MoveType.move) { - await storageService.deleteEmptyDirectories(selectionDirs); + showFeedback( + context, + copy ? l10n.collectionCopySuccessFeedback(count) : l10n.collectionMoveSuccessFeedback(count), + SnackBarAction( + label: context.l10n.showButtonLabel, + onPressed: () async { + final highlightInfo = context.read(); + var targetCollection = collection; + if (collection.filters.any((f) => f is AlbumFilter)) { + final filter = AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum)); + // we could simply add the filter to the current collection + // but navigating makes the change less jarring + targetCollection = CollectionLens( + source: collection.source, + filters: collection.filters, + groupFactor: collection.groupFactor, + sortFactor: collection.sortFactor, + )..addFilter(filter); + unawaited(Navigator.pushReplacement( + context, + MaterialPageRoute( + settings: const RouteSettings(name: CollectionPage.routeName), + builder: (context) { + return CollectionPage( + targetCollection, + ); + }, + ), + )); + await Future.delayed(Durations.staggeredAnimationPageTarget); + } + await Future.delayed(Durations.highlightScrollInitDelay); + final newUris = movedOps.map((v) => v.newFields['uri'] as String?).toSet(); + final targetEntry = targetCollection.sortedEntries.firstWhereOrNull((entry) => newUris.contains(entry.uri)); + if (targetEntry != null) { + highlightInfo.trackItem(targetEntry, highlightItem: targetEntry); + } + }, + ), + ); } }, ); } Future _showDeleteDialog(BuildContext context) async { - final selectionDirs = selection.where((e) => e.path != null).map((e) => e.directory).toSet(); + final collection = context.read(); + final source = collection.source; + final selection = collection.selection; + final selectionDirs = selection.where((e) => e.path != null).map((e) => e.directory).cast().toSet(); final todoCount = selection.length; final confirmed = await showDialog( diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart index 8fdc8cddd..0e5971f61 100644 --- a/lib/widgets/collection/filter_bar.dart +++ b/lib/widgets/collection/filter_bar.dart @@ -9,18 +9,18 @@ class FilterBar extends StatefulWidget implements PreferredSizeWidget { final List filters; final bool removable; - final FilterCallback onTap; + final FilterCallback? onTap; FilterBar({ - Key key, - @required Set filters, - @required this.removable, + Key? key, + required Set filters, + required this.removable, this.onTap, }) : filters = List.from(filters)..sort(), super(key: key); @override - final Size preferredSize = Size.fromHeight(preferredHeight); + final Size preferredSize = const Size.fromHeight(preferredHeight); @override _FilterBarState createState() => _FilterBarState(); @@ -28,9 +28,9 @@ class FilterBar extends StatefulWidget implements PreferredSizeWidget { class _FilterBarState extends State { final GlobalKey _animatedListKey = GlobalKey(debugLabel: 'filter-bar-animated-list'); - CollectionFilter _userTappedFilter; + CollectionFilter? _userTappedFilter; - FilterCallback get onTap => widget.onTap; + FilterCallback? get onTap => widget.onTap; @override void didUpdateWidget(covariant FilterBar oldWidget) { @@ -46,7 +46,7 @@ class _FilterBarState extends State { // only animate item removal when triggered by a user interaction with the chip, // not from automatic chip replacement following chip selection final animate = _userTappedFilter == filter; - listState.removeItem( + listState!.removeItem( index, animate ? (context, animation) { @@ -69,7 +69,7 @@ class _FilterBarState extends State { }); added.forEach((filter) { final index = current.indexOf(filter); - listState.insertItem( + listState!.insertItem( index, duration: Duration.zero, ); @@ -92,10 +92,10 @@ class _FilterBarState extends State { key: _animatedListKey, initialItemCount: widget.filters.length, scrollDirection: Axis.horizontal, - physics: BouncingScrollPhysics(), - padding: EdgeInsets.only(left: 8), + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(left: 8), itemBuilder: (context, index, animation) { - if (index >= widget.filters.length) return null; + if (index >= widget.filters.length) return const SizedBox(); return _buildChip(widget.filters.toList()[index]); }, ), @@ -105,7 +105,7 @@ class _FilterBarState extends State { Padding _buildChip(CollectionFilter filter) { return Padding( - padding: EdgeInsets.only(right: 8), + padding: const EdgeInsets.only(right: 8), child: Center( child: AvesFilterChip( key: ValueKey(filter), @@ -115,7 +115,7 @@ class _FilterBarState extends State { onTap: onTap != null ? (filter) { _userTappedFilter = filter; - onTap(filter); + onTap!(filter); } : null, ), diff --git a/lib/widgets/collection/grid/headers/album.dart b/lib/widgets/collection/grid/headers/album.dart index b2c5f3f93..5f0c7b313 100644 --- a/lib/widgets/collection/grid/headers/album.dart +++ b/lib/widgets/collection/grid/headers/album.dart @@ -2,37 +2,43 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/header.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:flutter/material.dart'; class AlbumSectionHeader extends StatelessWidget { - final String directory, albumName; + final String? directory, albumName; const AlbumSectionHeader({ - Key key, - @required this.directory, - @required this.albumName, + Key? key, + required this.directory, + required this.albumName, }) : super(key: key); @override Widget build(BuildContext context) { - var albumIcon = IconUtils.getAlbumIcon(context: context, album: directory); - if (albumIcon != null) { - albumIcon = Material( - type: MaterialType.circle, - elevation: 3, - color: Colors.transparent, - shadowColor: Colors.black, - child: albumIcon, - ); + Widget? albumIcon; + if (directory != null) { + albumIcon = IconUtils.getAlbumIcon(context: context, albumPath: directory!); + if (albumIcon != null) { + albumIcon = RepaintBoundary( + child: Material( + type: MaterialType.circle, + elevation: 3, + color: Colors.transparent, + shadowColor: Colors.black, + child: albumIcon, + ), + ); + } } return SectionHeader( sectionKey: EntryAlbumSectionKey(directory), leading: albumIcon, - title: albumName, - trailing: androidFileUtils.isOnRemovableStorage(directory) - ? Icon( + title: albumName ?? context.l10n.sectionUnknown, + trailing: directory != null && androidFileUtils.isOnRemovableStorage(directory!) + ? const Icon( AIcons.removableStorage, size: 16, color: Color(0xFF757575), @@ -42,7 +48,7 @@ class AlbumSectionHeader extends StatelessWidget { } static double getPreferredHeight(BuildContext context, double maxWidth, CollectionSource source, EntryAlbumSectionKey sectionKey) { - final directory = sectionKey.directory; + final directory = sectionKey.directory ?? context.l10n.sectionUnknown; return SectionHeader.getPreferredHeight( context: context, maxWidth: maxWidth, diff --git a/lib/widgets/collection/grid/headers/any.dart b/lib/widgets/collection/grid/headers/any.dart index 56f992c03..e245aec61 100644 --- a/lib/widgets/collection/grid/headers/any.dart +++ b/lib/widgets/collection/grid/headers/any.dart @@ -15,10 +15,10 @@ class CollectionSectionHeader extends StatelessWidget { final double height; const CollectionSectionHeader({ - Key key, - @required this.collection, - @required this.sectionKey, - @required this.height, + Key? key, + required this.collection, + required this.sectionKey, + required this.height, }) : super(key: key); @override @@ -29,10 +29,10 @@ class CollectionSectionHeader extends StatelessWidget { height: height, child: header, ) - : SizedBox.shrink(); + : const SizedBox.shrink(); } - Widget _buildHeader(BuildContext context) { + Widget? _buildHeader(BuildContext context) { switch (collection.sortFactor) { case EntrySortFactor.date: switch (collection.groupFactor) { @@ -60,7 +60,7 @@ class CollectionSectionHeader extends StatelessWidget { return AlbumSectionHeader( key: ValueKey(sectionKey), directory: directory, - albumName: source.getAlbumDisplayName(context, directory), + albumName: directory != null ? source.getAlbumDisplayName(context, directory) : null, ); } diff --git a/lib/widgets/collection/grid/headers/date.dart b/lib/widgets/collection/grid/headers/date.dart index 2e09e7874..983c84a23 100644 --- a/lib/widgets/collection/grid/headers/date.dart +++ b/lib/widgets/collection/grid/headers/date.dart @@ -6,11 +6,11 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class DaySectionHeader extends StatelessWidget { - final DateTime date; + final DateTime? date; const DaySectionHeader({ - Key key, - @required this.date, + Key? key, + required this.date, }) : super(key: key); // Examples (en_US): @@ -33,7 +33,7 @@ class DaySectionHeader extends StatelessWidget { // `MEd`: `1. 26. (화)` // `yMEd`: `2021. 1. 26. (화)` - static String _formatDate(BuildContext context, DateTime date) { + static String _formatDate(BuildContext context, DateTime? date) { final l10n = context.l10n; if (date == null) return l10n.sectionUnknown; if (date.isToday) return l10n.dateToday; @@ -53,14 +53,14 @@ class DaySectionHeader extends StatelessWidget { } class MonthSectionHeader extends StatelessWidget { - final DateTime date; + final DateTime? date; const MonthSectionHeader({ - Key key, - @required this.date, + Key? key, + required this.date, }) : super(key: key); - static String _formatDate(BuildContext context, DateTime date) { + static String _formatDate(BuildContext context, DateTime? date) { final l10n = context.l10n; if (date == null) return l10n.sectionUnknown; if (date.isThisMonth) return l10n.dateThisMonth; diff --git a/lib/widgets/collection/grid/section_layout.dart b/lib/widgets/collection/grid/section_layout.dart index 33597cbd5..785ea1cf4 100644 --- a/lib/widgets/collection/grid/section_layout.dart +++ b/lib/widgets/collection/grid/section_layout.dart @@ -3,23 +3,24 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/widgets/collection/grid/headers/any.dart'; import 'package:aves/widgets/common/grid/section_layout.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider { final CollectionLens collection; const SectionedEntryListLayoutProvider({ - @required this.collection, - @required double scrollableWidth, - @required int columnCount, - @required double tileExtent, - @required Widget Function(AvesEntry entry) tileBuilder, - @required Duration tileAnimationDelay, - @required Widget child, + required this.collection, + required double scrollableWidth, + required int columnCount, + required double spacing, + required double tileExtent, + required Widget Function(AvesEntry entry) tileBuilder, + required Duration tileAnimationDelay, + required Widget child, }) : super( scrollableWidth: scrollableWidth, columnCount: columnCount, + spacing: spacing, tileExtent: tileExtent, tileBuilder: tileBuilder, tileAnimationDelay: tileAnimationDelay, diff --git a/lib/widgets/collection/grid/selector.dart b/lib/widgets/collection/grid/selector.dart index be1f48d66..da1ee997a 100644 --- a/lib/widgets/collection/grid/selector.dart +++ b/lib/widgets/collection/grid/selector.dart @@ -19,10 +19,10 @@ class GridSelectionGestureDetector extends StatefulWidget { const GridSelectionGestureDetector({ this.selectable = true, - @required this.collection, - @required this.scrollController, - @required this.appBarHeightNotifier, - @required this.child, + required this.collection, + required this.scrollController, + required this.appBarHeightNotifier, + required this.child, }); @override @@ -30,16 +30,16 @@ class GridSelectionGestureDetector extends StatefulWidget { } class _GridSelectionGestureDetectorState extends State { - bool _pressing = false, _selecting; - int _fromIndex, _lastToIndex; - Offset _localPosition; - EdgeInsets _scrollableInsets; - double _scrollSpeedFactor; - Timer _updateTimer; + bool _pressing = false, _selecting = false; + late int _fromIndex, _lastToIndex; + late Offset _localPosition; + late EdgeInsets _scrollableInsets; + late double _scrollSpeedFactor; + Timer? _updateTimer; CollectionLens get collection => widget.collection; - List get entries => collection.sortedEntries; + List get entries => collection.sortedEntries; ScrollController get scrollController => widget.scrollController; @@ -102,7 +102,9 @@ class _GridSelectionGestureDetectorState extends State isScrollingNotifier; + final ValueNotifier? isScrollingNotifier; const InteractiveThumbnail({ - Key key, - this.collection, - @required this.entry, - @required this.tileExtent, + Key? key, + required this.collection, + required this.entry, + required this.tileExtent, this.isScrollingNotifier, }) : super(key: key); @@ -51,7 +51,7 @@ class InteractiveThumbnail extends StatelessWidget { metaData: ScalerMetadata(entry), child: DecoratedThumbnail( entry: entry, - extent: tileExtent, + tileExtent: tileExtent, collection: collection, // when the user is scrolling faster than we can retrieve the thumbnails, // the retrieval task queue can pile up for thumbnails that got disposed @@ -66,7 +66,7 @@ class InteractiveThumbnail extends StatelessWidget { Navigator.push( context, TransparentMaterialPageRoute( - settings: RouteSettings(name: EntryViewerPage.routeName), + settings: const RouteSettings(name: EntryViewerPage.routeName), pageBuilder: (c, a, sa) { final viewerCollection = CollectionLens( source: collection.source, @@ -76,6 +76,7 @@ class InteractiveThumbnail extends StatelessWidget { id: collection.id, listenToSource: false, ); + assert(viewerCollection.sortedEntries.contains(entry)); return EntryViewerPage( collection: viewerCollection, initialEntry: entry, diff --git a/lib/widgets/collection/thumbnail/decorated.dart b/lib/widgets/collection/thumbnail/decorated.dart index 5ecc75d95..442a067b7 100644 --- a/lib/widgets/collection/thumbnail/decorated.dart +++ b/lib/widgets/collection/thumbnail/decorated.dart @@ -3,22 +3,23 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/collection/thumbnail/overlay.dart'; import 'package:aves/widgets/collection/thumbnail/raster.dart'; import 'package:aves/widgets/collection/thumbnail/vector.dart'; +import 'package:aves/widgets/common/fx/borders.dart'; import 'package:flutter/material.dart'; class DecoratedThumbnail extends StatelessWidget { final AvesEntry entry; - final double extent; - final CollectionLens collection; - final ValueNotifier cancellableNotifier; + final double tileExtent; + final CollectionLens? collection; + final ValueNotifier? cancellableNotifier; final bool selectable, highlightable; static final Color borderColor = Colors.grey.shade700; - static const double borderWidth = .5; + static final double borderWidth = AvesBorder.borderWidth; const DecoratedThumbnail({ - Key key, - @required this.entry, - @required this.extent, + Key? key, + required this.entry, + required this.tileExtent, this.collection, this.cancellableNotifier, this.selectable = true, @@ -27,6 +28,8 @@ class DecoratedThumbnail extends StatelessWidget { @override Widget build(BuildContext context) { + final imageExtent = tileExtent - borderWidth * 2; + // hero tag should include a collection identifier, so that it animates // between different views of the entry in the same collection (e.g. thumbnails <-> viewer) // but not between different collection instances, even with the same attributes (e.g. reloading collection page via drawer) @@ -35,12 +38,12 @@ class DecoratedThumbnail extends StatelessWidget { var child = isSvg ? VectorImageThumbnail( entry: entry, - extent: extent, + extent: imageExtent, heroTag: heroTag, ) : RasterImageThumbnail( entry: entry, - extent: extent, + extent: imageExtent, cancellableNotifier: cancellableNotifier, heroTag: heroTag, ); @@ -49,33 +52,21 @@ class DecoratedThumbnail extends StatelessWidget { alignment: isSvg ? Alignment.center : AlignmentDirectional.bottomStart, children: [ child, - if (!isSvg) - ThumbnailEntryOverlay( - entry: entry, - extent: extent, - ), - if (selectable) - ThumbnailSelectionOverlay( - entry: entry, - extent: extent, - ), - if (highlightable) - ThumbnailHighlightOverlay( - entry: entry, - extent: extent, - ), + if (!isSvg) ThumbnailEntryOverlay(entry: entry), + if (selectable) ThumbnailSelectionOverlay(entry: entry), + if (highlightable) ThumbnailHighlightOverlay(entry: entry), ], ); return Container( - foregroundDecoration: BoxDecoration( - border: Border.all( + decoration: BoxDecoration( + border: Border.fromBorderSide(BorderSide( color: borderColor, width: borderWidth, - ), + )), ), - width: extent, - height: extent, + width: tileExtent, + height: tileExtent, child: child, ); } diff --git a/lib/widgets/collection/thumbnail/error.dart b/lib/widgets/collection/thumbnail/error.dart index 21849a5c3..7345a4198 100644 --- a/lib/widgets/collection/thumbnail/error.dart +++ b/lib/widgets/collection/thumbnail/error.dart @@ -13,9 +13,9 @@ class ErrorThumbnail extends StatefulWidget { final String tooltip; const ErrorThumbnail({ - @required this.entry, - @required this.extent, - @required this.tooltip, + required this.entry, + required this.extent, + required this.tooltip, }); @override @@ -23,7 +23,7 @@ class ErrorThumbnail extends StatefulWidget { } class _ErrorThumbnailState extends State { - Future _exists; + late Future _exists; AvesEntry get entry => widget.entry; @@ -32,7 +32,7 @@ class _ErrorThumbnailState extends State { @override void initState() { super.initState(); - _exists = entry.path != null ? File(entry.path).exists() : SynchronousFuture(true); + _exists = entry.path != null ? File(entry.path!).exists() : SynchronousFuture(true); } @override @@ -41,12 +41,12 @@ class _ErrorThumbnailState extends State { return FutureBuilder( future: _exists, builder: (context, snapshot) { - if (snapshot.connectionState != ConnectionState.done) return SizedBox(); - final exists = snapshot.data; - return Container( - alignment: Alignment.center, - color: Colors.black, - child: Tooltip( + Widget child; + if (snapshot.connectionState != ConnectionState.done) { + child = const SizedBox(); + } else { + final exists = snapshot.data!; + child = Tooltip( message: exists ? widget.tooltip : context.l10n.viewerErrorDoesNotExist, preferBelow: false, child: exists @@ -63,7 +63,14 @@ class _ErrorThumbnailState extends State { size: extent / 2, color: color, ), - ), + ); + } + return Container( + alignment: Alignment.center, + color: Colors.black, + width: extent, + height: extent, + child: child, ); }); } diff --git a/lib/widgets/collection/thumbnail/overlay.dart b/lib/widgets/collection/thumbnail/overlay.dart index 133088e95..4da8d33f3 100644 --- a/lib/widgets/collection/thumbnail/overlay.dart +++ b/lib/widgets/collection/thumbnail/overlay.dart @@ -14,32 +14,30 @@ import 'package:provider/provider.dart'; class ThumbnailEntryOverlay extends StatelessWidget { final AvesEntry entry; - final double extent; const ThumbnailEntryOverlay({ - Key key, - @required this.entry, - @required this.extent, + Key? key, + required this.entry, }) : super(key: key); @override Widget build(BuildContext context) { final children = [ - if (entry.hasGps && context.select((t) => t.showLocation)) GpsIcon(), + if (entry.hasGps && context.select((t) => t.showLocation)) const GpsIcon(), if (entry.isVideo) VideoIcon( entry: entry, ) else if (entry.isAnimated) - AnimatedImageIcon() + const AnimatedImageIcon() else ...[ - if (entry.isRaw && context.select((t) => t.showRaw)) RawIcon(), + if (entry.isRaw && context.select((t) => t.showRaw)) const RawIcon(), if (entry.isMultiPage) MultiPageIcon(entry: entry), - if (entry.isGeotiff) GeotiffIcon(), - if (entry.is360) SphericalImageIcon(), + if (entry.isGeotiff) const GeotiffIcon(), + if (entry.is360) const SphericalImageIcon(), ] ]; - if (children.isEmpty) return SizedBox.shrink(); + if (children.isEmpty) return const SizedBox.shrink(); if (children.length == 1) return children.first; return Column( mainAxisSize: MainAxisSize.min, @@ -51,14 +49,12 @@ class ThumbnailEntryOverlay extends StatelessWidget { class ThumbnailSelectionOverlay extends StatelessWidget { final AvesEntry entry; - final double extent; static const duration = Durations.thumbnailOverlayAnimation; const ThumbnailSelectionOverlay({ - Key key, - @required this.entry, - @required this.extent, + Key? key, + required this.entry, }) : super(key: key); @override @@ -78,7 +74,7 @@ class ThumbnailSelectionOverlay extends StatelessWidget { icon: selected ? AIcons.selected : AIcons.unselected, size: context.select((t) => t.iconSize), ) - : SizedBox.shrink(); + : const SizedBox.shrink(); child = AnimatedSwitcher( duration: duration, switchInCurve: Curves.easeOutBack, @@ -98,7 +94,7 @@ class ThumbnailSelectionOverlay extends StatelessWidget { return child; }, ) - : SizedBox.shrink(); + : const SizedBox.shrink(); return AnimatedSwitcher( duration: duration, child: child, @@ -110,12 +106,10 @@ class ThumbnailSelectionOverlay extends StatelessWidget { class ThumbnailHighlightOverlay extends StatefulWidget { final AvesEntry entry; - final double extent; const ThumbnailHighlightOverlay({ - Key key, - @required this.entry, - @required this.extent, + Key? key, + required this.entry, }) : super(key: key); @override @@ -136,10 +130,10 @@ class _ThumbnailHighlightOverlayState extends State { return Sweeper( builder: (context) => Container( decoration: BoxDecoration( - border: Border.all( + border: Border.fromBorderSide(BorderSide( color: Theme.of(context).accentColor, - width: widget.extent * .1, - ), + width: context.select((t) => t.highlightBorderWidth), + )), ), ), toggledNotifier: _highlightedNotifier, diff --git a/lib/widgets/collection/thumbnail/raster.dart b/lib/widgets/collection/thumbnail/raster.dart index 400fa5b8d..779e2f57d 100644 --- a/lib/widgets/collection/thumbnail/raster.dart +++ b/lib/widgets/collection/thumbnail/raster.dart @@ -1,7 +1,10 @@ +import 'dart:math'; +import 'dart:ui'; + import 'package:aves/image_providers/thumbnail_provider.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; -import 'package:aves/theme/durations.dart'; +import 'package:aves/services/services.dart'; import 'package:aves/widgets/collection/thumbnail/error.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/transition_image.dart'; @@ -10,13 +13,17 @@ import 'package:flutter/material.dart'; class RasterImageThumbnail extends StatefulWidget { final AvesEntry entry; final double extent; - final ValueNotifier cancellableNotifier; - final Object heroTag; + final BoxFit fit; + final bool showLoadingBackground; + final ValueNotifier? cancellableNotifier; + final Object? heroTag; const RasterImageThumbnail({ - Key key, - @required this.entry, - @required this.extent, + Key? key, + required this.entry, + required this.extent, + this.fit = BoxFit.cover, + this.showLoadingBackground = true, this.cancellableNotifier, this.heroTag, }) : super(key: key); @@ -26,7 +33,12 @@ class RasterImageThumbnail extends StatefulWidget { } class _RasterImageThumbnailState extends State { - ThumbnailProvider _fastThumbnailProvider, _sizedThumbnailProvider; + final _providers = <_ConditionalImageProvider>[]; + _ProviderStream? _currentProviderStream; + ImageInfo? _lastImageInfo; + Object? _lastException; + late final ImageStreamListener _streamListener; + late DisposableBuildContext> _scrollAwareContext; AvesEntry get entry => widget.entry; @@ -35,6 +47,8 @@ class _RasterImageThumbnailState extends State { @override void initState() { super.initState(); + _streamListener = ImageStreamListener(_onImageLoad, onError: _onError); + _scrollAwareContext = DisposableBuildContext>(this); _registerWidget(widget); } @@ -50,6 +64,7 @@ class _RasterImageThumbnailState extends State { @override void dispose() { _unregisterWidget(widget); + _scrollAwareContext.dispose(); super.dispose(); } @@ -61,72 +76,125 @@ class _RasterImageThumbnailState extends State { void _unregisterWidget(RasterImageThumbnail widget) { widget.entry.imageChangeNotifier.removeListener(_onImageChanged); _pauseProvider(); + _currentProviderStream?.stopListening(); + _currentProviderStream = null; + _replaceImage(null); } void _initProvider() { if (!entry.canDecode) return; - _fastThumbnailProvider = entry.getThumbnail(); - _sizedThumbnailProvider = entry.getThumbnail(extent: extent); + _lastException = null; + _providers.clear(); + _providers.addAll([ + _ConditionalImageProvider( + ScrollAwareImageProvider( + context: _scrollAwareContext, + imageProvider: entry.getThumbnail(), + ), + ), + _ConditionalImageProvider( + ScrollAwareImageProvider( + context: _scrollAwareContext, + imageProvider: entry.getThumbnail(extent: extent), + ), + _needSizedProvider, + ), + ]); + _loadNextProvider(); } - void _pauseProvider() { - if (widget.cancellableNotifier?.value ?? false) { - _fastThumbnailProvider?.pause(); - _sizedThumbnailProvider?.pause(); + void _loadNextProvider([ImageInfo? imageInfo]) { + final nextIndex = _currentProviderStream == null ? 0 : (_providers.indexOf(_currentProviderStream!.provider) + 1); + if (nextIndex < _providers.length) { + final provider = _providers[nextIndex]; + if (provider.predicate?.call(imageInfo) ?? true) { + _currentProviderStream?.stopListening(); + _currentProviderStream = _ProviderStream(provider, _streamListener); + _currentProviderStream!.startListening(); + } } } + void _onImageLoad(ImageInfo imageInfo, bool synchronousCall) { + _replaceImage(imageInfo); + _loadNextProvider(imageInfo); + } + + void _replaceImage(ImageInfo? imageInfo) { + _lastImageInfo?.dispose(); + _lastImageInfo = imageInfo; + if (imageInfo != null) { + setState(() {}); + } + } + + void _onError(Object exception, StackTrace? stackTrace) { + if (mounted) { + setState(() => _lastException = exception); + } + } + + bool _needSizedProvider(ImageInfo? currentImageInfo) { + if (currentImageInfo == null) return true; + final currentImage = currentImageInfo.image; + // directly uses `devicePixelRatio` as it never changes, to avoid visiting ancestors via `MediaQuery` + final sizedThreshold = extent * window.devicePixelRatio; + return sizedThreshold > min(currentImage.width, currentImage.height); + } + + void _pauseProvider() async { + if (widget.cancellableNotifier?.value ?? false) { + final key = await _currentProviderStream?.provider.provider.obtainKey(ImageConfiguration.empty); + if (key is ThumbnailProviderKey) { + imageFileService.cancelThumbnail(key); + } + } + } + + Color? _backgroundColor; + + Color get backgroundColor { + if (_backgroundColor == null) { + final rgb = 0x30 + entry.uri.hashCode % 0x20; + _backgroundColor = Color.fromARGB(0xFF, rgb, rgb, rgb); + } + return _backgroundColor!; + } + @override Widget build(BuildContext context) { if (!entry.canDecode) { return _buildError(context, context.l10n.errorUnsupportedMimeType(entry.mimeType), null); + } else if (_lastException != null) { + return _buildError(context, _lastException.toString(), null); } - final fastImage = Image( - key: ValueKey('LQ'), - image: _fastThumbnailProvider, - errorBuilder: _buildError, - width: extent, - height: extent, - fit: BoxFit.cover, - ); - final image = _sizedThumbnailProvider == null - ? fastImage - : Image( - key: ValueKey('HQ'), - image: _sizedThumbnailProvider, - frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { - if (wasSynchronouslyLoaded) return child; - return AnimatedSwitcher( - duration: Durations.thumbnailTransition, - transitionBuilder: (child, animation) { - var shouldFade = true; - if (child is Image && child.image == _fastThumbnailProvider) { - // directly show LQ thumbnail, only fade when switching from LQ to HQ - shouldFade = false; - } - return shouldFade - ? FadeTransition( - opacity: animation, - child: child, - ) - : child; - }, - child: frame == null ? fastImage : child, - ); - }, - errorBuilder: _buildError, + // use `RawImage` instead of `Image`, using `ImageInfo` to check dimensions + // and have more control when chaining image providers + + final imageInfo = _lastImageInfo; + final image = imageInfo == null + ? Container( + color: widget.showLoadingBackground ? backgroundColor : Colors.transparent, width: extent, height: extent, - fit: BoxFit.cover, + ) + : RawImage( + image: imageInfo.image, + debugImageLabel: imageInfo.debugLabel, + width: extent, + height: extent, + scale: imageInfo.scale, + fit: widget.fit, ); + return widget.heroTag != null ? Hero( - tag: widget.heroTag, + tag: widget.heroTag!, flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { return TransitionImage( - image: entry.getBestThumbnail(extent), + image: entry.bestCachedThumbnail, animation: animation, ); }, @@ -136,7 +204,7 @@ class _RasterImageThumbnailState extends State { : image; } - Widget _buildError(BuildContext context, Object error, StackTrace stackTrace) => ErrorThumbnail( + Widget _buildError(BuildContext context, Object error, StackTrace? stackTrace) => ErrorThumbnail( entry: entry, extent: extent, tooltip: error.toString(), @@ -150,3 +218,22 @@ class _RasterImageThumbnailState extends State { setState(() {}); } } + +class _ConditionalImageProvider { + final ImageProvider provider; + final bool Function(ImageInfo?)? predicate; + + const _ConditionalImageProvider(this.provider, [this.predicate]); +} + +class _ProviderStream { + final _ConditionalImageProvider provider; + final ImageStream _stream; + final ImageStreamListener listener; + + _ProviderStream(this.provider, this.listener) : _stream = provider.provider.resolve(ImageConfiguration.empty); + + void startListening() => _stream.addListener(listener); + + void stopListening() => _stream.removeListener(listener); +} diff --git a/lib/widgets/collection/thumbnail/theme.dart b/lib/widgets/collection/thumbnail/theme.dart index fa85901f4..12e92f90d 100644 --- a/lib/widgets/collection/thumbnail/theme.dart +++ b/lib/widgets/collection/thumbnail/theme.dart @@ -6,13 +6,13 @@ import 'package:provider/provider.dart'; class ThumbnailTheme extends StatelessWidget { final double extent; - final bool showLocation; + final bool? showLocation; final Widget child; const ThumbnailTheme({ - @required this.extent, + required this.extent, this.showLocation, - @required this.child, + required this.child, }); @override @@ -21,9 +21,11 @@ class ThumbnailTheme extends StatelessWidget { update: (_, settings, __) { final iconSize = min(28.0, (extent / 4)).roundToDouble(); final fontSize = (iconSize / 2).floorToDouble(); + final highlightBorderWidth = extent * .1; return ThumbnailThemeData( iconSize: iconSize, fontSize: fontSize, + highlightBorderWidth: highlightBorderWidth, showLocation: showLocation ?? settings.showThumbnailLocation, showRaw: settings.showThumbnailRaw, showVideoDuration: settings.showThumbnailVideoDuration, @@ -35,14 +37,15 @@ class ThumbnailTheme extends StatelessWidget { } class ThumbnailThemeData { - final double iconSize, fontSize; + final double iconSize, fontSize, highlightBorderWidth; final bool showLocation, showRaw, showVideoDuration; const ThumbnailThemeData({ - @required this.iconSize, - @required this.fontSize, - @required this.showLocation, - @required this.showRaw, - @required this.showVideoDuration, + required this.iconSize, + required this.fontSize, + required this.highlightBorderWidth, + required this.showLocation, + required this.showRaw, + required this.showVideoDuration, }); } diff --git a/lib/widgets/collection/thumbnail/vector.dart b/lib/widgets/collection/thumbnail/vector.dart index 099780779..2c71b3593 100644 --- a/lib/widgets/collection/thumbnail/vector.dart +++ b/lib/widgets/collection/thumbnail/vector.dart @@ -11,12 +11,12 @@ import 'package:provider/provider.dart'; class VectorImageThumbnail extends StatelessWidget { final AvesEntry entry; final double extent; - final Object heroTag; + final Object? heroTag; const VectorImageThumbnail({ - Key key, - @required this.entry, - @required this.extent, + Key? key, + required this.entry, + required this.extent, this.heroTag, }) : super(key: key); @@ -31,7 +31,7 @@ class VectorImageThumbnail extends StatelessWidget { builder: (context, constraints) { final availableSize = constraints.biggest; final fitSize = applyBoxFit(fit, entry.displaySize, availableSize).destination; - final offset = fitSize / 2 - availableSize / 2; + final offset = (fitSize / 2 - availableSize / 2) as Offset; final child = CustomPaint( painter: CheckeredPainter(checkSize: extent / 8, offset: offset), child: SvgPicture( @@ -66,7 +66,7 @@ class VectorImageThumbnail extends StatelessWidget { ); return heroTag != null ? Hero( - tag: heroTag, + tag: heroTag!, transitionOnUserGestures: true, child: child, ) diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index 4600de46c..99cd02a54 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -6,27 +6,28 @@ import 'package:percent_indicator/circular_percent_indicator.dart'; mixin FeedbackMixin { void dismissFeedback(BuildContext context) => ScaffoldMessenger.of(context).hideCurrentSnackBar(); - void showFeedback(BuildContext context, String message) { - showFeedbackWithMessenger(ScaffoldMessenger.of(context), message); + void showFeedback(BuildContext context, String message, [SnackBarAction? action]) { + showFeedbackWithMessenger(ScaffoldMessenger.of(context), message, action); } // provide the messenger if feedback happens as the widget is disposed - void showFeedbackWithMessenger(ScaffoldMessengerState messenger, String message) { + void showFeedbackWithMessenger(ScaffoldMessengerState messenger, String message, [SnackBarAction? action]) { messenger.showSnackBar(SnackBar( content: Text(message), - duration: Durations.opToastDisplay, + action: action, + duration: action != null ? Durations.opToastActionDisplay : Durations.opToastDisplay, )); } // report overlay for multiple operations void showOpReport({ - @required BuildContext context, - @required Stream opStream, - @required int itemCount, - void Function(Set processed) onDone, + required BuildContext context, + required Stream opStream, + required int itemCount, + void Function(Set processed)? onDone, }) { - OverlayEntry _opReportOverlayEntry; + late OverlayEntry _opReportOverlayEntry; _opReportOverlayEntry = OverlayEntry( builder: (context) => ReportOverlay( opStream: opStream, @@ -37,7 +38,7 @@ mixin FeedbackMixin { }, ), ); - Overlay.of(context).insert(_opReportOverlayEntry); + Overlay.of(context)!.insert(_opReportOverlayEntry); } } @@ -47,9 +48,9 @@ class ReportOverlay extends StatefulWidget { final void Function(Set processed) onDone; const ReportOverlay({ - @required this.opStream, - @required this.itemCount, - @required this.onDone, + required this.opStream, + required this.itemCount, + required this.onDone, }); @override @@ -58,8 +59,8 @@ class ReportOverlay extends StatefulWidget { class _ReportOverlayState extends State> with SingleTickerProviderStateMixin { final processed = {}; - AnimationController _animationController; - Animation _animation; + late AnimationController _animationController; + late Animation _animation; Stream get opStream => widget.opStream; @@ -103,7 +104,7 @@ class _ReportOverlayState extends State> with SingleTickerPr return FadeTransition( opacity: _animation, child: Container( - decoration: BoxDecoration( + decoration: const BoxDecoration( gradient: RadialGradient( colors: [ Colors.black, diff --git a/lib/widgets/common/action_mixins/permission_aware.dart b/lib/widgets/common/action_mixins/permission_aware.dart index 7c2d91955..89e75d179 100644 --- a/lib/widgets/common/action_mixins/permission_aware.dart +++ b/lib/widgets/common/action_mixins/permission_aware.dart @@ -3,21 +3,21 @@ import 'package:aves/services/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; mixin PermissionAwareMixin { Future checkStoragePermission(BuildContext context, Set entries) { - return checkStoragePermissionForAlbums(context, entries.where((e) => e.path != null).map((e) => e.directory).toSet()); + return checkStoragePermissionForAlbums(context, entries.where((e) => e.path != null).map((e) => e.directory).cast().toSet()); } Future checkStoragePermissionForAlbums(BuildContext context, Set albumPaths) async { final restrictedDirs = await storageService.getRestrictedDirectories(); while (true) { final dirs = await storageService.getInaccessibleDirectories(albumPaths); - if (dirs == null) return false; if (dirs.isEmpty) return true; - final restrictedInaccessibleDir = dirs.firstWhere(restrictedDirs.contains, orElse: () => null); + final restrictedInaccessibleDir = dirs.firstWhereOrNull(restrictedDirs.contains); if (restrictedInaccessibleDir != null) { await showRestrictedDirectoryDialog(context, restrictedInaccessibleDir); return false; @@ -57,7 +57,7 @@ mixin PermissionAwareMixin { } } - Future showRestrictedDirectoryDialog(BuildContext context, VolumeRelativeDirectory dir) { + Future showRestrictedDirectoryDialog(BuildContext context, VolumeRelativeDirectory dir) { return showDialog( context: context, builder: (context) { diff --git a/lib/widgets/common/action_mixins/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart index 95b0e338a..6a8b7f3f8 100644 --- a/lib/widgets/common/action_mixins/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -19,10 +19,15 @@ mixin SizeAwareMixin { String destinationAlbum, MoveType moveType, ) async { + // assume we have enough space if we cannot find the volume or its remaining free space final destinationVolume = androidFileUtils.getStorageVolume(destinationAlbum); + if (destinationVolume == null) return true; + final free = await storageService.getFreeSpace(destinationVolume); - int needed; - int sumSize(sum, entry) => sum + entry.sizeBytes; + if (free == null) return true; + + late int needed; + int sumSize(sum, entry) => sum + entry.sizeBytes ?? 0; switch (moveType) { case MoveType.copy: case MoveType.export: @@ -30,11 +35,11 @@ mixin SizeAwareMixin { break; case MoveType.move: // when moving, we only need space for the entries that are not already on the destination volume - final byVolume = groupBy(selection, (entry) => androidFileUtils.getStorageVolume(entry.path)); + final byVolume = Map.fromEntries(groupBy(selection, (entry) => androidFileUtils.getStorageVolume(entry.path)).entries.where((kv) => kv.key != null).cast>>()); final otherVolumes = byVolume.keys.where((volume) => volume != destinationVolume); - final fromOtherVolumes = otherVolumes.fold(0, (sum, volume) => sum + byVolume[volume].fold(0, sumSize)); + final fromOtherVolumes = otherVolumes.fold(0, (sum, volume) => sum + byVolume[volume]!.fold(0, sumSize)); // and we need at least as much space as the largest entry because individual entries are copied then deleted - final largestSingle = selection.fold(0, (largest, entry) => max(largest, entry.sizeBytes)); + final largestSingle = selection.fold(0, (largest, entry) => max(largest, entry.sizeBytes ?? 0)); needed = max(fromOtherVolumes, largestSingle); break; } diff --git a/lib/widgets/common/app_bar_subtitle.dart b/lib/widgets/common/app_bar_subtitle.dart index 575a92d95..abf2761a5 100644 --- a/lib/widgets/common/app_bar_subtitle.dart +++ b/lib/widgets/common/app_bar_subtitle.dart @@ -9,9 +9,9 @@ class SourceStateAwareAppBarTitle extends StatelessWidget { final CollectionSource source; const SourceStateAwareAppBarTitle({ - Key key, - @required this.title, - @required this.source, + Key? key, + required this.title, + required this.source, }) : super(key: key); @override @@ -34,7 +34,7 @@ class SourceStateAwareAppBarTitle extends StatelessWidget { ), ), child: sourceState == SourceState.ready - ? SizedBox.shrink() + ? const SizedBox.shrink() : SourceStateSubtitle( source: source, ), @@ -49,11 +49,11 @@ class SourceStateAwareAppBarTitle extends StatelessWidget { class SourceStateSubtitle extends StatelessWidget { final CollectionSource source; - const SourceStateSubtitle({@required this.source}); + const SourceStateSubtitle({required this.source}); @override Widget build(BuildContext context) { - String subtitle; + String? subtitle; switch (source.stateNotifier.value) { case SourceState.loading: subtitle = context.l10n.sourceStateLoading; @@ -70,7 +70,7 @@ class SourceStateSubtitle extends StatelessWidget { } final subtitleStyle = Theme.of(context).textTheme.caption; return subtitle == null - ? SizedBox.shrink() + ? const SizedBox.shrink() : Row( mainAxisSize: MainAxisSize.min, children: [ @@ -78,13 +78,13 @@ class SourceStateSubtitle extends StatelessWidget { StreamBuilder( stream: source.progressStream, builder: (context, snapshot) { - if (snapshot.hasError || !snapshot.hasData) return SizedBox.shrink(); - final progress = snapshot.data; + if (snapshot.hasError || !snapshot.hasData) return const SizedBox.shrink(); + final progress = snapshot.data!; return Padding( - padding: EdgeInsetsDirectional.only(start: 8), + padding: const EdgeInsetsDirectional.only(start: 8), child: Text( '${progress.done}/${progress.total}', - style: subtitleStyle.copyWith(color: Colors.white30), + style: subtitleStyle!.copyWith(color: Colors.white30), ), ); }, diff --git a/lib/widgets/common/app_bar_title.dart b/lib/widgets/common/app_bar_title.dart index ed285d341..5b0c25330 100644 --- a/lib/widgets/common/app_bar_title.dart +++ b/lib/widgets/common/app_bar_title.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; class InteractiveAppBarTitle extends StatelessWidget { - final GestureTapCallback onTap; + final GestureTapCallback? onTap; final Widget child; const InteractiveAppBarTitle({ this.onTap, - @required this.child, + required this.child, }); @override @@ -17,7 +17,7 @@ class InteractiveAppBarTitle extends StatelessWidget { // so that we can also detect taps around the title `Text` child: Container( alignment: AlignmentDirectional.centerStart, - padding: EdgeInsets.symmetric(horizontal: NavigationToolbar.kMiddleSpacing), + 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 13df2ac11..876604465 100644 --- a/lib/widgets/common/aves_highlight.dart +++ b/lib/widgets/common/aves_highlight.dart @@ -14,7 +14,7 @@ class AvesHighlightView extends StatelessWidget { /// It is recommended to give it a value for performance /// /// [All available languages](https://github.com/pd4d10/highlight/tree/master/highlight/lib/languages) - final String language; + final String? language; /// Highlight theme /// @@ -22,12 +22,12 @@ class AvesHighlightView extends StatelessWidget { final Map theme; /// Padding - final EdgeInsetsGeometry padding; + final EdgeInsetsGeometry? padding; /// Text styles /// /// Specify text styles such as font family and font size - final TextStyle textStyle; + final TextStyle? textStyle; AvesHighlightView( String input, { @@ -45,16 +45,16 @@ class AvesHighlightView extends StatelessWidget { void _traverse(Node node) { if (node.value != null) { - currentSpans.add(node.className == null ? TextSpan(text: node.value) : TextSpan(text: node.value, style: theme[node.className])); + currentSpans.add(node.className == null ? TextSpan(text: node.value) : TextSpan(text: node.value, style: theme[node.className!])); } else if (node.children != null) { final tmp = []; - currentSpans.add(TextSpan(children: tmp, style: theme[node.className])); + currentSpans.add(TextSpan(children: tmp, style: theme[node.className!])); stack.add(currentSpans); currentSpans = tmp; - node.children.forEach((n) { + node.children!.forEach((n) { _traverse(n); - if (n == node.children.last) { + if (n == node.children!.last) { currentSpans = stack.isEmpty ? spans : stack.removeLast(); } }); @@ -93,7 +93,7 @@ class AvesHighlightView extends StatelessWidget { child: SelectableText.rich( TextSpan( style: _textStyle, - children: _convert(highlight.parse(source, language: language).nodes), + children: _convert(highlight.parse(source, language: language).nodes!), ), ), ); diff --git a/lib/widgets/common/basic/draggable_scrollbar.dart b/lib/widgets/common/basic/draggable_scrollbar.dart index e6666d51f..1f061a425 100644 --- a/lib/widgets/common/basic/draggable_scrollbar.dart +++ b/lib/widgets/common/basic/draggable_scrollbar.dart @@ -18,7 +18,7 @@ typedef ScrollThumbBuilder = Widget Function( Animation thumbAnimation, Animation labelAnimation, double height, { - Widget labelText, + Widget? labelText, }); /// Build a Text widget using the current scroll offset @@ -37,7 +37,7 @@ class DraggableScrollbar extends StatefulWidget { final ScrollThumbBuilder scrollThumbBuilder; /// The amount of padding that should surround the thumb - final EdgeInsetsGeometry padding; + final EdgeInsets? padding; /// Determines how quickly the scrollbar will animate in and out final Duration scrollbarAnimationDuration; @@ -46,7 +46,7 @@ class DraggableScrollbar extends StatefulWidget { final Duration scrollbarTimeToFade; /// Build a Text widget from the current offset in the BoxScrollView - final LabelTextBuilder labelTextBuilder; + final LabelTextBuilder? labelTextBuilder; /// The ScrollController for the BoxScrollView final ScrollController controller; @@ -55,30 +55,28 @@ class DraggableScrollbar extends StatefulWidget { final ScrollView child; DraggableScrollbar({ - Key key, - @required this.backgroundColor, - @required this.scrollThumbHeight, - @required this.scrollThumbBuilder, - @required this.controller, + Key? key, + required this.backgroundColor, + required this.scrollThumbHeight, + required this.scrollThumbBuilder, + required this.controller, this.padding, this.scrollbarAnimationDuration = const Duration(milliseconds: 300), this.scrollbarTimeToFade = const Duration(milliseconds: 1000), this.labelTextBuilder, - @required this.child, - }) : assert(controller != null), - assert(scrollThumbBuilder != null), - assert(child.scrollDirection == Axis.vertical), + required this.child, + }) : assert(child.scrollDirection == Axis.vertical), super(key: key); @override _DraggableScrollbarState createState() => _DraggableScrollbarState(); static Widget buildScrollThumbAndLabel({ - @required Widget scrollThumb, - @required Color backgroundColor, - @required Animation thumbAnimation, - @required Animation labelAnimation, - @required Widget labelText, + required Widget scrollThumb, + required Color backgroundColor, + required Animation thumbAnimation, + required Animation labelAnimation, + required Widget? labelText, }) { final scrollThumbAndLabel = labelText == null ? scrollThumb @@ -91,7 +89,7 @@ class DraggableScrollbar extends StatefulWidget { backgroundColor: backgroundColor, child: labelText, ), - SizedBox(width: 24), + const SizedBox(width: 24), scrollThumb, ], ); @@ -108,10 +106,10 @@ class ScrollLabel extends StatelessWidget { final Widget child; const ScrollLabel({ - Key key, - @required this.child, - @required this.animation, - @required this.backgroundColor, + Key? key, + required this.child, + required this.animation, + required this.backgroundColor, }) : super(key: key); @override @@ -119,11 +117,11 @@ class ScrollLabel extends StatelessWidget { return FadeTransition( opacity: animation, child: Container( - margin: EdgeInsets.only(right: 12.0), + margin: const EdgeInsets.only(right: 12.0), child: Material( elevation: 4.0, color: backgroundColor, - borderRadius: BorderRadius.circular(16), + borderRadius: const BorderRadius.all(Radius.circular(16)), child: child, ), ), @@ -134,13 +132,13 @@ class ScrollLabel extends StatelessWidget { class _DraggableScrollbarState extends State with TickerProviderStateMixin { final ValueNotifier _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0); bool _isDragInProcess = false; - Offset _longPressLastGlobalPosition; + late Offset _longPressLastGlobalPosition; - AnimationController _thumbAnimationController; - Animation _thumbAnimation; - AnimationController _labelAnimationController; - Animation _labelAnimation; - Timer _fadeoutTimer; + late AnimationController _thumbAnimationController; + late Animation _thumbAnimation; + late AnimationController _labelAnimationController; + late Animation _labelAnimation; + Timer? _fadeoutTimer; @override void initState() { @@ -177,7 +175,7 @@ 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 thumbMaxScrollExtent => context.size!.height - widget.scrollThumbHeight - (widget.padding?.vertical ?? 0.0); double get thumbMinScrollExtent => 0.0; @@ -208,20 +206,20 @@ class _DraggableScrollbarState extends State with TickerProv onVerticalDragStart: (_) => _onVerticalDragStart(), onVerticalDragUpdate: (details) => _onVerticalDragUpdate(details.delta.dy), onVerticalDragEnd: (_) => _onVerticalDragEnd(), - child: ValueListenableBuilder( + child: ValueListenableBuilder( valueListenable: _thumbOffsetNotifier, builder: (context, thumbOffset, child) => Container( alignment: AlignmentDirectional.topEnd, - padding: EdgeInsets.only(top: thumbOffset) + widget.padding, + padding: EdgeInsets.only(top: thumbOffset) + (widget.padding ?? EdgeInsets.zero), child: widget.scrollThumbBuilder( widget.backgroundColor, _thumbAnimation, _labelAnimation, widget.scrollThumbHeight, labelText: (widget.labelTextBuilder != null && _isDragInProcess) - ? ValueListenableBuilder( + ? ValueListenableBuilder( valueListenable: _viewOffsetNotifier, - builder: (context, viewOffset, child) => widget.labelTextBuilder(viewOffset + thumbOffset), + builder: (context, viewOffset, child) => widget.labelTextBuilder!.call(viewOffset + thumbOffset), ) : null, ), @@ -376,20 +374,20 @@ class SlideFadeTransition extends StatelessWidget { final Widget child; const SlideFadeTransition({ - Key key, - @required this.animation, - @required this.child, + Key? key, + required this.animation, + required this.child, }) : super(key: key); @override Widget build(BuildContext context) { return AnimatedBuilder( animation: animation, - builder: (context, child) => animation.value == 0.0 ? Container() : child, + builder: (context, child) => animation.value == 0.0 ? Container() : child!, child: SlideTransition( position: Tween( - begin: Offset(0.3, 0.0), - end: Offset(0.0, 0.0), + begin: const Offset(0.3, 0.0), + end: const Offset(0.0, 0.0), ).animate(animation), child: FadeTransition( opacity: animation, diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart index 9bf5b4c35..47ef0bdd2 100644 --- a/lib/widgets/common/basic/insets.dart +++ b/lib/widgets/common/basic/insets.dart @@ -17,7 +17,7 @@ class BottomGestureAreaProtector extends StatelessWidget { right: 0, bottom: 0, height: systemGestureBottom, - child: AbsorbPointer(), + child: const AbsorbPointer(), ); }, ); @@ -27,7 +27,7 @@ class BottomGestureAreaProtector extends StatelessWidget { class GestureAreaProtectorStack extends StatelessWidget { final Widget child; - const GestureAreaProtectorStack({@required this.child}); + const GestureAreaProtectorStack({required this.child}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/basic/labeled_checkbox.dart b/lib/widgets/common/basic/labeled_checkbox.dart index c8b2d4e87..fa8205602 100644 --- a/lib/widgets/common/basic/labeled_checkbox.dart +++ b/lib/widgets/common/basic/labeled_checkbox.dart @@ -3,14 +3,14 @@ import 'package:flutter/material.dart'; class LabeledCheckbox extends StatefulWidget { final bool value; - final ValueChanged onChanged; + final ValueChanged onChanged; final String text; const LabeledCheckbox({ - Key key, - @required this.value, - @required this.onChanged, - @required this.text, + Key? key, + required this.value, + required this.onChanged, + required this.text, }) : super(key: key); @override @@ -18,7 +18,7 @@ class LabeledCheckbox extends StatefulWidget { } class _LabeledCheckboxState extends State { - TapGestureRecognizer _tapRecognizer; + late TapGestureRecognizer _tapRecognizer; @override void initState() { diff --git a/lib/widgets/common/basic/link_chip.dart b/lib/widgets/common/basic/link_chip.dart index d64258966..d1437531f 100644 --- a/lib/widgets/common/basic/link_chip.dart +++ b/lib/widgets/common/basic/link_chip.dart @@ -3,19 +3,19 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; class LinkChip extends StatelessWidget { - final Widget leading; + final Widget? leading; final String text; final String url; - final Color color; - final TextStyle textStyle; + final Color? color; + final TextStyle? textStyle; - static final borderRadius = BorderRadius.circular(8); + static const borderRadius = BorderRadius.all(Radius.circular(8)); const LinkChip({ - Key key, + Key? key, this.leading, - @required this.text, - @required this.url, + required this.text, + required this.url, this.color, this.textStyle, }) : super(key: key); @@ -23,7 +23,7 @@ class LinkChip extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultTextStyle.merge( - style: (textStyle ?? TextStyle()).copyWith(color: color), + style: (textStyle ?? const TextStyle()).copyWith(color: color), child: InkWell( borderRadius: borderRadius, onTap: () async { @@ -32,13 +32,13 @@ class LinkChip extends StatelessWidget { } }, child: Padding( - padding: EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8.0), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (leading != null) ...[ - leading, - SizedBox(width: 8), + leading!, + const SizedBox(width: 8), ], Flexible( child: Text( @@ -48,7 +48,7 @@ class LinkChip extends StatelessWidget { maxLines: 1, ), ), - SizedBox(width: 8), + const SizedBox(width: 8), Builder( builder: (context) => Icon( AIcons.openOutside, diff --git a/lib/widgets/common/basic/menu_row.dart b/lib/widgets/common/basic/menu_row.dart index bcc3acf06..81492121a 100644 --- a/lib/widgets/common/basic/menu_row.dart +++ b/lib/widgets/common/basic/menu_row.dart @@ -3,12 +3,12 @@ import 'package:flutter/material.dart'; class MenuRow extends StatelessWidget { final String text; - final IconData icon; - final bool checked; + final IconData? icon; + final bool? checked; const MenuRow({ - Key key, - this.text, + Key? key, + required this.text, this.icon, this.checked, }) : super(key: key); @@ -16,19 +16,19 @@ class MenuRow extends StatelessWidget { @override Widget build(BuildContext context) { final textScaleFactor = MediaQuery.textScaleFactorOf(context); - final iconSize = IconTheme.of(context).size * textScaleFactor; + final iconSize = IconTheme.of(context).size! * textScaleFactor; return Row( children: [ if (checked != null) ...[ Opacity( - opacity: checked ? 1 : 0, + opacity: checked! ? 1 : 0, child: Icon(AIcons.checked, size: iconSize), ), - SizedBox(width: 8), + const SizedBox(width: 8), ], if (icon != null) ...[ Icon(icon, size: iconSize), - SizedBox(width: 8), + const SizedBox(width: 8), ], Expanded(child: Text(text)), ], diff --git a/lib/widgets/common/basic/multi_cross_fader.dart b/lib/widgets/common/basic/multi_cross_fader.dart index 22d75f822..5736bf622 100644 --- a/lib/widgets/common/basic/multi_cross_fader.dart +++ b/lib/widgets/common/basic/multi_cross_fader.dart @@ -7,11 +7,11 @@ class MultiCrossFader extends StatefulWidget { final Widget child; const MultiCrossFader({ - @required this.duration, + required this.duration, this.fadeCurve = Curves.linear, this.sizeCurve = Curves.linear, this.alignment = Alignment.topCenter, - @required this.child, + required this.child, }); @override @@ -19,14 +19,14 @@ class MultiCrossFader extends StatefulWidget { } class _MultiCrossFaderState extends State { - Widget _first, _second; + late Widget _first, _second; CrossFadeState _fadeState = CrossFadeState.showFirst; @override void initState() { super.initState(); _first = widget.child; - _second = SizedBox(); + _second = const SizedBox(); } @override diff --git a/lib/widgets/common/basic/outlined_text.dart b/lib/widgets/common/basic/outlined_text.dart index 088d2a229..6e9a36701 100644 --- a/lib/widgets/common/basic/outlined_text.dart +++ b/lib/widgets/common/basic/outlined_text.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; typedef OutlinedWidgetBuilder = Widget Function(BuildContext context, bool isShadow); class OutlinedText extends StatelessWidget { - final OutlinedWidgetBuilder leadingBuilder, trailingBuilder; + final OutlinedWidgetBuilder? leadingBuilder, trailingBuilder; final String text; final TextStyle style; final double outlineWidth; @@ -12,13 +12,13 @@ class OutlinedText extends StatelessWidget { static const widgetSpanAlignment = PlaceholderAlignment.middle; const OutlinedText({ - Key key, + Key? key, this.leadingBuilder, - @required this.text, + required this.text, this.trailingBuilder, - @required this.style, - double outlineWidth, - Color outlineColor, + required this.style, + double? outlineWidth, + Color? outlineColor, }) : outlineWidth = outlineWidth ?? 1, outlineColor = outlineColor ?? Colors.black, super(key: key); @@ -33,7 +33,7 @@ class OutlinedText extends StatelessWidget { if (leadingBuilder != null) WidgetSpan( alignment: widgetSpanAlignment, - child: leadingBuilder(context, true), + child: leadingBuilder!(context, true), ), TextSpan( text: text, @@ -47,7 +47,7 @@ class OutlinedText extends StatelessWidget { if (trailingBuilder != null) WidgetSpan( alignment: widgetSpanAlignment, - child: trailingBuilder(context, true), + child: trailingBuilder!(context, true), ), ], ), @@ -58,7 +58,7 @@ class OutlinedText extends StatelessWidget { if (leadingBuilder != null) WidgetSpan( alignment: widgetSpanAlignment, - child: leadingBuilder(context, false), + child: leadingBuilder!(context, false), ), TextSpan( text: text, @@ -67,7 +67,7 @@ class OutlinedText extends StatelessWidget { if (trailingBuilder != null) WidgetSpan( alignment: widgetSpanAlignment, - child: trailingBuilder(context, false), + child: trailingBuilder!(context, false), ), ], ), diff --git a/lib/widgets/common/basic/query_bar.dart b/lib/widgets/common/basic/query_bar.dart index aa3834857..2a35774b5 100644 --- a/lib/widgets/common/basic/query_bar.dart +++ b/lib/widgets/common/basic/query_bar.dart @@ -9,7 +9,7 @@ import 'package:flutter/widgets.dart'; class QueryBar extends StatefulWidget { final ValueNotifier filterNotifier; - const QueryBar({@required this.filterNotifier}); + const QueryBar({required this.filterNotifier}); @override _QueryBarState createState() => _QueryBarState(); @@ -17,7 +17,7 @@ class QueryBar extends StatefulWidget { class _QueryBarState extends State { final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay); - TextEditingController _controller; + late TextEditingController _controller; ValueNotifier get filterNotifier => widget.filterNotifier; @@ -30,7 +30,7 @@ class _QueryBarState extends State { @override Widget build(BuildContext context) { final clearButton = IconButton( - icon: Icon(AIcons.clear), + icon: const Icon(AIcons.clear), onPressed: () { _controller.clear(); filterNotifier.value = ''; @@ -45,7 +45,7 @@ class _QueryBarState extends State { child: TextField( controller: _controller, decoration: InputDecoration( - icon: Padding( + icon: const Padding( padding: EdgeInsetsDirectional.only(start: 16), child: Icon(AIcons.search), ), @@ -57,7 +57,7 @@ class _QueryBarState extends State { ), ), ConstrainedBox( - constraints: BoxConstraints(minWidth: 16), + constraints: const BoxConstraints(minWidth: 16), child: ValueListenableBuilder( valueListenable: _controller, builder: (context, value, child) => AnimatedSwitcher( @@ -70,7 +70,7 @@ class _QueryBarState extends State { child: child, ), ), - child: value.text.isNotEmpty ? clearButton : SizedBox.shrink(), + child: value.text.isNotEmpty ? clearButton : const SizedBox.shrink(), ), ), ) diff --git a/lib/widgets/common/basic/reselectable_radio_list_tile.dart b/lib/widgets/common/basic/reselectable_radio_list_tile.dart index f90a4a159..c6c546b37 100644 --- a/lib/widgets/common/basic/reselectable_radio_list_tile.dart +++ b/lib/widgets/common/basic/reselectable_radio_list_tile.dart @@ -4,15 +4,15 @@ import 'package:flutter/material.dart'; class ReselectableRadioListTile extends StatelessWidget { final T value; final T groupValue; - final ValueChanged onChanged; + final ValueChanged? onChanged; final bool toggleable; final bool reselectable; - final Color activeColor; - final Widget title; - final Widget subtitle; - final Widget secondary; + final Color? activeColor; + final Widget? title; + final Widget? subtitle; + final Widget? secondary; final bool isThreeLine; - final bool dense; + final bool? dense; final bool selected; final ListTileControlAffinity controlAffinity; final bool autofocus; @@ -20,10 +20,10 @@ class ReselectableRadioListTile extends StatelessWidget { bool get checked => value == groupValue; const ReselectableRadioListTile({ - Key key, - @required this.value, - @required this.groupValue, - @required this.onChanged, + Key? key, + required this.value, + required this.groupValue, + required this.onChanged, this.toggleable = false, this.reselectable = false, this.activeColor, @@ -35,12 +35,7 @@ class ReselectableRadioListTile extends StatelessWidget { this.selected = false, this.controlAffinity = ListTileControlAffinity.platform, this.autofocus = false, - }) : assert(toggleable != null), - assert(isThreeLine != null), - assert(!isThreeLine || subtitle != null), - assert(selected != null), - assert(controlAffinity != null), - assert(autofocus != null), + }) : assert(!isThreeLine || subtitle != null), super(key: key); @override @@ -54,7 +49,7 @@ class ReselectableRadioListTile extends StatelessWidget { materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, autofocus: autofocus, ); - Widget leading, trailing; + Widget? leading, trailing; switch (controlAffinity) { case ListTileControlAffinity.leading: case ListTileControlAffinity.platform: @@ -80,11 +75,11 @@ class ReselectableRadioListTile extends StatelessWidget { onTap: onChanged != null ? () { if (toggleable && checked) { - onChanged(null); + onChanged!(null); return; } if (reselectable || !checked) { - onChanged(value); + onChanged!(value); } } : null, diff --git a/lib/widgets/common/behaviour/double_back_pop.dart b/lib/widgets/common/behaviour/double_back_pop.dart index e3aa98d5c..8118bea01 100644 --- a/lib/widgets/common/behaviour/double_back_pop.dart +++ b/lib/widgets/common/behaviour/double_back_pop.dart @@ -12,7 +12,7 @@ class DoubleBackPopScope extends StatefulWidget { final Widget child; const DoubleBackPopScope({ - @required this.child, + required this.child, }); @override @@ -21,7 +21,7 @@ class DoubleBackPopScope extends StatefulWidget { class _DoubleBackPopScopeState extends State with FeedbackMixin { bool _backOnce = false; - Timer _backTimer; + Timer? _backTimer; @override void dispose() { diff --git a/lib/widgets/common/behaviour/route_tracker.dart b/lib/widgets/common/behaviour/route_tracker.dart index 3be10ae63..c0d577919 100644 --- a/lib/widgets/common/behaviour/route_tracker.dart +++ b/lib/widgets/common/behaviour/route_tracker.dart @@ -3,16 +3,16 @@ import 'package:flutter/material.dart'; class CrashlyticsRouteTracker extends NavigatorObserver { @override - void didPush(Route route, Route previousRoute) => FirebaseCrashlytics.instance.log('Nav didPush to ${_name(route)}'); + void didPush(Route route, Route? previousRoute) => FirebaseCrashlytics.instance.log('Nav didPush to ${_name(route)}'); @override - void didPop(Route route, Route previousRoute) => FirebaseCrashlytics.instance.log('Nav didPop to ${_name(previousRoute)}'); + void didPop(Route route, Route? previousRoute) => FirebaseCrashlytics.instance.log('Nav didPop to ${_name(previousRoute)}'); @override - void didRemove(Route route, Route previousRoute) => FirebaseCrashlytics.instance.log('Nav didRemove to ${_name(previousRoute)}'); + void didRemove(Route route, Route? previousRoute) => FirebaseCrashlytics.instance.log('Nav didRemove to ${_name(previousRoute)}'); @override - void didReplace({Route newRoute, Route oldRoute}) => FirebaseCrashlytics.instance.log('Nav didReplace to ${_name(newRoute)}'); + void didReplace({Route? newRoute, Route? oldRoute}) => FirebaseCrashlytics.instance.log('Nav didReplace to ${_name(newRoute)}'); - String _name(Route route) => route?.settings?.name ?? 'unnamed ${route?.runtimeType}'; + String _name(Route? route) => route?.settings.name ?? 'unnamed ${route?.runtimeType}'; } diff --git a/lib/widgets/common/behaviour/routes.dart b/lib/widgets/common/behaviour/routes.dart index a5ac9d079..0051022bf 100644 --- a/lib/widgets/common/behaviour/routes.dart +++ b/lib/widgets/common/behaviour/routes.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; class DirectMaterialPageRoute extends PageRouteBuilder { DirectMaterialPageRoute({ - RouteSettings settings, - @required WidgetBuilder builder, + RouteSettings? settings, + required WidgetBuilder builder, }) : super( settings: settings, transitionDuration: Duration.zero, @@ -18,8 +18,8 @@ class DirectMaterialPageRoute extends PageRouteBuilder { class TransparentMaterialPageRoute extends PageRouteBuilder { TransparentMaterialPageRoute({ - RouteSettings settings, - @required RoutePageBuilder pageBuilder, + RouteSettings? settings, + required RoutePageBuilder pageBuilder, }) : super(settings: settings, pageBuilder: pageBuilder); @override diff --git a/lib/widgets/common/behaviour/sloppy_scroll_physics.dart b/lib/widgets/common/behaviour/sloppy_scroll_physics.dart index 1fbb8f158..072c57d4d 100644 --- a/lib/widgets/common/behaviour/sloppy_scroll_physics.dart +++ b/lib/widgets/common/behaviour/sloppy_scroll_physics.dart @@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart'; class SloppyScrollPhysics extends ScrollPhysics { const SloppyScrollPhysics({ this.touchSlopFactor = 1, - ScrollPhysics parent, + ScrollPhysics? parent, }) : super(parent: parent); // in [0, 1] @@ -13,7 +13,7 @@ class SloppyScrollPhysics extends ScrollPhysics { final double touchSlopFactor; @override - SloppyScrollPhysics applyTo(ScrollPhysics ancestor) { + SloppyScrollPhysics applyTo(ScrollPhysics? ancestor) { return SloppyScrollPhysics( touchSlopFactor: touchSlopFactor, parent: buildParent(ancestor), diff --git a/lib/widgets/common/extensions/build_context.dart b/lib/widgets/common/extensions/build_context.dart index bd79ec90f..02a4bfb5b 100644 --- a/lib/widgets/common/extensions/build_context.dart +++ b/lib/widgets/common/extensions/build_context.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; extension ExtraContext on BuildContext { - String get currentRouteName => ModalRoute.of(this)?.settings?.name; + String? get currentRouteName => ModalRoute.of(this)?.settings.name; - AppLocalizations get l10n => AppLocalizations.of(this); + AppLocalizations get l10n => AppLocalizations.of(this)!; } diff --git a/lib/widgets/common/extensions/media_query.dart b/lib/widgets/common/extensions/media_query.dart index e0e15959c..f3a1cfc9f 100644 --- a/lib/widgets/common/extensions/media_query.dart +++ b/lib/widgets/common/extensions/media_query.dart @@ -4,7 +4,9 @@ import 'package:flutter/widgets.dart'; extension ExtraMediaQueryData on MediaQueryData { /* - examples of MediaQuery props in practice, as of Flutter v1.22.5 + examples of MediaQuery props in practice + + -- Flutter v1.22.5 S20, Android 11, portrait, notch top, button nav bar bottom padding EdgeInsets(0.0, 26.0, 0.0, 48.0) @@ -35,6 +37,28 @@ extension ExtraMediaQueryData on MediaQueryData { padding EdgeInsets(0.0, 24.0, 0.0, 0.0) viewPadding EdgeInsets(0.0, 24.0, 0.0, 0.0) viewInsets EdgeInsets.zero + + -- Flutter v2.2.1 + + S10e, Android 11, portrait, notch top, button nav bar bottom, keyboard off + padding EdgeInsets(0.0, 39.0, 0.0, 48.0) + viewPadding EdgeInsets(0.0, 39.0, 0.0, 48.0) + viewInsets EdgeInsets.zero + + S10e, Android 11, portrait, notch top, button nav bar bottom, keyboard on + padding EdgeInsets(0.0, 39.0, 0.0, 0.0) + viewPadding EdgeInsets(0.0, 39.0, 0.0, 48.0) + viewInsets EdgeInsets(0.0, 0.0, 0.0, 338.0) + + S10e, Android 11, portrait, notch top, gesture nav bar bottom, keyboard off + padding EdgeInsets(0.0, 39.0, 0.0, 15.0) + viewPadding EdgeInsets(0.0, 39.0, 0.0, 15.0) + viewInsets EdgeInsets.zero + + S10e, Android 11, portrait, notch top, gesture nav bar bottom, keyboard on + padding EdgeInsets(0.0, 39.0, 0.0, 0.0) + viewPadding EdgeInsets(0.0, 39.0, 0.0, 15.0) + viewInsets EdgeInsets(0.0, 0.0, 0.0, 338.0) */ double get effectiveBottomPadding => max(viewPadding.bottom, viewInsets.bottom); diff --git a/lib/widgets/common/fx/blurred.dart b/lib/widgets/common/fx/blurred.dart index d14d3e214..c995a4597 100644 --- a/lib/widgets/common/fx/blurred.dart +++ b/lib/widgets/common/fx/blurred.dart @@ -9,9 +9,9 @@ class BlurredRect extends StatelessWidget { final Widget child; const BlurredRect({ - Key key, + Key? key, this.enabled = true, - this.child, + required this.child, }) : super(key: key); @override @@ -31,12 +31,16 @@ class BlurredRRect extends StatelessWidget { final double borderRadius; final Widget child; - const BlurredRRect({Key key, this.borderRadius, this.child}) : super(key: key); + const BlurredRRect({ + Key? key, + required this.borderRadius, + required this.child, + }) : super(key: key); @override Widget build(BuildContext context) { return ClipRRect( - borderRadius: BorderRadius.circular(borderRadius), + borderRadius: BorderRadius.all(Radius.circular(borderRadius)), child: BackdropFilter( filter: _filter, child: child, @@ -48,7 +52,10 @@ class BlurredRRect extends StatelessWidget { class BlurredOval extends StatelessWidget { final Widget child; - const BlurredOval({Key key, this.child}) : super(key: key); + const BlurredOval({ + Key? key, + required this.child, + }) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/fx/borders.dart b/lib/widgets/common/fx/borders.dart index 70ba4b054..830cdd4b5 100644 --- a/lib/widgets/common/fx/borders.dart +++ b/lib/widgets/common/fx/borders.dart @@ -1,19 +1,17 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'dart:ui'; -class AvesCircleBorder { +import 'package:flutter/material.dart'; + +class AvesBorder { static const borderColor = Colors.white30; - static double _borderWidth(BuildContext context) => context.read().devicePixelRatio > 2 ? 0.5 : 1.0; + // directly uses `devicePixelRatio` as it never changes, to avoid visiting ancestors via `MediaQuery` + static double get borderWidth => window.devicePixelRatio > 2 ? 0.5 : 1.0; - static Border build(BuildContext context) { - return Border.fromBorderSide(buildSide(context)); - } + static BorderSide get side => BorderSide( + color: borderColor, + width: borderWidth, + ); - static BorderSide buildSide(BuildContext context) { - return BorderSide( - color: borderColor, - width: _borderWidth(context), - ); - } + static Border get border => Border.fromBorderSide(side); } diff --git a/lib/widgets/common/fx/highlight_decoration.dart b/lib/widgets/common/fx/highlight_decoration.dart index 096d139cd..669fdd94d 100644 --- a/lib/widgets/common/fx/highlight_decoration.dart +++ b/lib/widgets/common/fx/highlight_decoration.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.dart'; class HighlightDecoration extends Decoration { final Color color; - const HighlightDecoration({@required this.color}); + const HighlightDecoration({required this.color}); @override - _HighlightDecorationPainter createBoxPainter([VoidCallback onChanged]) { + _HighlightDecorationPainter createBoxPainter([VoidCallback? onChanged]) { return _HighlightDecorationPainter(this, onChanged); } } @@ -14,11 +14,13 @@ class HighlightDecoration extends Decoration { class _HighlightDecorationPainter extends BoxPainter { final HighlightDecoration decoration; - const _HighlightDecorationPainter(this.decoration, VoidCallback onChanged) : super(onChanged); + const _HighlightDecorationPainter(this.decoration, VoidCallback? onChanged) : super(onChanged); @override void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { final size = configuration.size; + if (size == null) return; + final confHeight = size.height; final paintHeight = confHeight * .4; final rect = Rect.fromLTWH(offset.dx, offset.dy + confHeight - paintHeight, size.width, paintHeight); diff --git a/lib/widgets/common/fx/sweeper.dart b/lib/widgets/common/fx/sweeper.dart index 01221faf5..a724f0ddb 100644 --- a/lib/widgets/common/fx/sweeper.dart +++ b/lib/widgets/common/fx/sweeper.dart @@ -12,15 +12,15 @@ class Sweeper extends StatefulWidget { final Curve curve; final ValueNotifier toggledNotifier; final bool centerSweep; - final VoidCallback onSweepEnd; + final VoidCallback? onSweepEnd; const Sweeper({ - Key key, - @required this.builder, + Key? key, + required this.builder, this.startAngle = -pi / 2, this.sweepAngle = pi / 4, this.curve = Curves.easeInOutCubic, - @required this.toggledNotifier, + required this.toggledNotifier, this.centerSweep = true, this.onSweepEnd, }) : super(key: key); @@ -30,8 +30,8 @@ class Sweeper extends StatefulWidget { } class _SweeperState extends State with SingleTickerProviderStateMixin { - AnimationController _angleAnimationController; - Animation _angle; + late AnimationController _angleAnimationController; + late Animation _angle; bool _isAppearing = false; bool get isToggled => widget.toggledNotifier.value; @@ -129,7 +129,7 @@ class _SweepClipPath extends CustomClipper { final double startAngle; final double sweepAngle; - const _SweepClipPath({@required this.startAngle, @required this.sweepAngle}); + const _SweepClipPath({required this.startAngle, required this.sweepAngle}); @override Path getClip(Size size) { diff --git a/lib/widgets/common/fx/transition_image.dart b/lib/widgets/common/fx/transition_image.dart index 2a62162b7..9c96574f1 100644 --- a/lib/widgets/common/fx/transition_image.dart +++ b/lib/widgets/common/fx/transition_image.dart @@ -10,13 +10,13 @@ import 'package:flutter/material.dart'; class TransitionImage extends StatefulWidget { final ImageProvider image; - final double width, height; + final double? width, height; final ValueListenable animation; final bool gaplessPlayback = false; const TransitionImage({ - @required this.image, - @required this.animation, + required this.image, + required this.animation, this.width, this.height, }); @@ -26,10 +26,10 @@ class TransitionImage extends StatefulWidget { } class _TransitionImageState extends State { - ImageStream _imageStream; - ImageInfo _imageInfo; + ImageStream? _imageStream; + ImageInfo? _imageInfo; bool _isListeningToStream = false; - int _frameNumber; + int? _frameNumber; @override void initState() { @@ -60,8 +60,8 @@ class _TransitionImageState extends State { void didUpdateWidget(covariant TransitionImage oldWidget) { super.didUpdateWidget(oldWidget); if (_isListeningToStream) { - _imageStream.removeListener(_getListener()); - _imageStream.addListener(_getListener()); + _imageStream!.removeListener(_getListener()); + _imageStream!.addListener(_getListener()); } if (widget.image != oldWidget.image) _resolveImage(); } @@ -76,9 +76,8 @@ class _TransitionImageState extends State { final provider = widget.image; final newStream = provider.resolve(createLocalImageConfiguration( context, - size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null, + size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null, )); - assert(newStream != null); _updateSourceStream(newStream); } @@ -92,7 +91,7 @@ class _TransitionImageState extends State { void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) { setState(() { _imageInfo = imageInfo; - _frameNumber = _frameNumber == null ? 0 : _frameNumber + 1; + _frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1; }); } @@ -100,9 +99,9 @@ class _TransitionImageState extends State { // registration from the old stream to the new stream (if a listener was // registered). void _updateSourceStream(ImageStream newStream) { - if (_imageStream?.key == newStream?.key) return; + if (_imageStream?.key == newStream.key) return; - if (_isListeningToStream) _imageStream.removeListener(_getListener()); + if (_isListeningToStream) _imageStream!.removeListener(_getListener()); if (!widget.gaplessPlayback) { setState(() { @@ -115,18 +114,18 @@ class _TransitionImageState extends State { }); _imageStream = newStream; - if (_isListeningToStream) _imageStream.addListener(_getListener()); + if (_isListeningToStream) _imageStream!.addListener(_getListener()); } void _listenToStream() { if (_isListeningToStream) return; - _imageStream.addListener(_getListener()); + _imageStream!.addListener(_getListener()); _isListeningToStream = true; } void _stopListeningToStream() { if (!_isListeningToStream) return; - _imageStream.removeListener(_getListener()); + _imageStream!.removeListener(_getListener()); _isListeningToStream = false; } @@ -147,14 +146,14 @@ class _TransitionImageState extends State { } class _TransitionImagePainter extends CustomPainter { - final ui.Image image; + final ui.Image? image; final double scale; final double t; const _TransitionImagePainter({ - @required this.image, - @required this.scale, - @required this.t, + required this.image, + required this.scale, + required this.t, }); @override @@ -167,13 +166,13 @@ class _TransitionImagePainter extends CustomPainter { const alignment = Alignment.center; final rect = ui.Rect.fromLTWH(0, 0, size.width, size.height); - final inputSize = Size(image.width.toDouble(), image.height.toDouble()); + final inputSize = Size(image!.width.toDouble(), image!.height.toDouble()); final outputSize = rect.size; final coverSizes = applyBoxFit(BoxFit.cover, inputSize / scale, size); final containSizes = applyBoxFit(BoxFit.contain, inputSize / scale, size); - final sourceSize = Size.lerp(coverSizes.source, containSizes.source, t) * scale; - final destinationSize = Size.lerp(coverSizes.destination, containSizes.destination, t); + final sourceSize = Size.lerp(coverSizes.source, containSizes.source, t)! * scale; + final destinationSize = Size.lerp(coverSizes.destination, containSizes.destination, t)!; final halfWidthDelta = (outputSize.width - destinationSize.width) / 2.0; final halfHeightDelta = (outputSize.height - destinationSize.height) / 2.0; @@ -185,7 +184,7 @@ class _TransitionImagePainter extends CustomPainter { sourceSize, Offset.zero & inputSize, ); - canvas.drawImageRect(image, sourceRect, destinationRect, paint); + canvas.drawImageRect(image!, sourceRect, destinationRect, paint); } @override diff --git a/lib/widgets/common/grid/draggable_thumb_label.dart b/lib/widgets/common/grid/draggable_thumb_label.dart index fa84ebb5a..d82161d3a 100644 --- a/lib/widgets/common/grid/draggable_thumb_label.dart +++ b/lib/widgets/common/grid/draggable_thumb_label.dart @@ -9,29 +9,28 @@ class DraggableThumbLabel extends StatelessWidget { final List Function(BuildContext context, T item) lineBuilder; const DraggableThumbLabel({ - @required this.offsetY, - @required this.lineBuilder, + required this.offsetY, + required this.lineBuilder, }); @override Widget build(BuildContext context) { final sll = context.read>(); final sectionLayout = sll.getSectionAt(offsetY); - if (sectionLayout == null) return SizedBox(); + if (sectionLayout == null) return const SizedBox(); - final section = sll.sections[sectionLayout.sectionKey]; + final section = sll.sections[sectionLayout.sectionKey]!; final dy = offsetY - (sectionLayout.minOffset + sectionLayout.headerExtent); final itemIndex = dy < 0 ? 0 : (dy ~/ (sll.tileExtent + sll.spacing)) * sll.columnCount; final item = section[itemIndex]; - if (item == null) return SizedBox(); final lines = lineBuilder(context, item); - if (lines.isEmpty) return SizedBox(); + if (lines.isEmpty) return const SizedBox(); return ConstrainedBox( - constraints: BoxConstraints(maxWidth: 140), + constraints: const BoxConstraints(maxWidth: 140), child: Padding( - padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), child: lines.length > 1 ? Column( mainAxisSize: MainAxisSize.min, @@ -45,7 +44,7 @@ class DraggableThumbLabel extends StatelessWidget { Widget _buildText(String text) => Text( text, - style: TextStyle( + style: const TextStyle( color: Colors.black, ), softWrap: false, @@ -53,13 +52,13 @@ class DraggableThumbLabel extends StatelessWidget { maxLines: 1, ); - static String formatMonthThumbLabel(BuildContext context, DateTime date) { + static String formatMonthThumbLabel(BuildContext context, DateTime? date) { final l10n = context.l10n; if (date == null) return l10n.sectionUnknown; return DateFormat.yMMM(l10n.localeName).format(date); } - static String formatDayThumbLabel(BuildContext context, DateTime date) { + static String formatDayThumbLabel(BuildContext context, DateTime? date) { final l10n = context.l10n; if (date == null) return l10n.sectionUnknown; return DateFormat.yMMMd(l10n.localeName).format(date); diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart index 2ca2500e4..37ac83dd4 100644 --- a/lib/widgets/common/grid/header.dart +++ b/lib/widgets/common/grid/header.dart @@ -11,15 +11,15 @@ import 'package:provider/provider.dart'; class SectionHeader extends StatelessWidget { final SectionKey sectionKey; - final Widget leading, trailing; + final Widget? leading, trailing; final String title; final bool selectable; const SectionHeader({ - Key key, - @required this.sectionKey, + Key? key, + required this.sectionKey, this.leading, - @required this.title, + required this.title, this.trailing, this.selectable = true, }) : super(key: key); @@ -35,7 +35,7 @@ class SectionHeader extends StatelessWidget { return Container( alignment: AlignmentDirectional.centerStart, padding: padding, - constraints: BoxConstraints(minHeight: leadingDimension), + constraints: const BoxConstraints(minHeight: leadingDimension), child: GestureDetector( onTap: selectable ? () => _toggleSectionSelection(context) : null, child: Text.rich( @@ -78,7 +78,7 @@ class SectionHeader extends StatelessWidget { void _toggleSectionSelection(BuildContext context) { final collection = context.read(); - final sectionEntries = collection.sections[sectionKey]; + final sectionEntries = collection.sections[sectionKey]!; final selected = collection.isSelected(sectionEntries); if (selected) { collection.removeFromSelection(sectionEntries); @@ -89,9 +89,9 @@ class SectionHeader extends StatelessWidget { // TODO TLAD cache header extent computation? static double getPreferredHeight({ - @required BuildContext context, - @required double maxWidth, - @required String title, + required BuildContext context, + required double maxWidth, + required String title, bool hasLeading = false, bool hasTrailing = false, }) { @@ -124,15 +124,15 @@ class SectionHeader extends StatelessWidget { class _SectionSelectableLeading extends StatelessWidget { final bool selectable; final SectionKey sectionKey; - final WidgetBuilder browsingBuilder; - final VoidCallback onPressed; + final WidgetBuilder? browsingBuilder; + final VoidCallback? onPressed; const _SectionSelectableLeading({ - Key key, + Key? key, this.selectable = true, - @required this.sectionKey, - @required this.browsingBuilder, - @required this.onPressed, + required this.sectionKey, + required this.browsingBuilder, + required this.onPressed, }) : super(key: key); static const leadingDimension = SectionHeader.leadingDimension; @@ -149,7 +149,7 @@ class _SectionSelectableLeading extends StatelessWidget { ? AnimatedBuilder( animation: collection.selectionChangeNotifier, builder: (context, child) { - final sectionEntries = collection.sections[sectionKey]; + final sectionEntries = collection.sections[sectionKey]!; final selected = collection.isSelected(sectionEntries); final child = TooltipTheme( key: ValueKey(selected), @@ -158,12 +158,12 @@ class _SectionSelectableLeading extends StatelessWidget { ), child: IconButton( iconSize: 26, - padding: EdgeInsets.only(top: 1), + padding: const EdgeInsets.only(top: 1), alignment: AlignmentDirectional.topStart, icon: Icon(selected ? AIcons.selected : AIcons.unselected), onPressed: onPressed, tooltip: selected ? context.l10n.collectionDeselectSectionTooltip : context.l10n.collectionSelectSectionTooltip, - constraints: BoxConstraints( + constraints: const BoxConstraints( minHeight: leadingDimension, minWidth: leadingDimension, ), @@ -208,5 +208,5 @@ class _SectionSelectableLeading extends StatelessWidget { ); } - Widget _buildBrowsing(BuildContext context) => browsingBuilder?.call(context) ?? SizedBox(height: leadingDimension); + Widget _buildBrowsing(BuildContext context) => browsingBuilder?.call(context) ?? const SizedBox(height: leadingDimension); } diff --git a/lib/widgets/common/grid/item_tracker.dart b/lib/widgets/common/grid/item_tracker.dart new file mode 100644 index 000000000..d9d16b3a5 --- /dev/null +++ b/lib/widgets/common/grid/item_tracker.dart @@ -0,0 +1,144 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:aves/model/highlight.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/grid/section_layout.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +mixin GridItemTrackerMixin on State, WidgetsBindingObserver { + ValueNotifier get appBarHeightNotifier; + + ScrollController get scrollController; + + GlobalKey get scrollableKey; + + Size get scrollableSize { + final scrollableContext = scrollableKey.currentContext!; + return (scrollableContext.findRenderObject() as RenderBox).size; + } + + Orientation get _windowOrientation { + final size = WidgetsBinding.instance!.window.physicalSize; + return size.width > size.height ? Orientation.landscape : Orientation.portrait; + } + + final List _subscriptions = []; + + // grid section metrics before the app is laid out with the new orientation + late SectionedListLayout _lastSectionedListLayout; + late Size _lastScrollableSize; + late Orientation _lastOrientation; + + @override + void initState() { + super.initState(); + final highlightInfo = context.read(); + _subscriptions.add(highlightInfo.eventBus.on>().listen(_trackItem)); + _lastOrientation = _windowOrientation; + WidgetsBinding.instance!.addObserver(this); + _saveLayoutMetrics(); + } + + @override + void didUpdateWidget(covariant oldWidget) { + super.didUpdateWidget(oldWidget); + _saveLayoutMetrics(); + } + + @override + void dispose() { + WidgetsBinding.instance!.removeObserver(this); + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + super.dispose(); + } + + // about scrolling & offset retrieval: + // `Scrollable.ensureVisible` only works on already rendered objects + // `RenderViewport.showOnScreen` can find any `RenderSliver`, but not always a `RenderMetadata` + // `RenderViewport.scrollOffsetOf` is a good alternative + Future _trackItem(TrackEvent event) async { + final sectionedListLayout = context.read>(); + final tileRect = sectionedListLayout.getTileRect(event.item); + if (tileRect == null) return; + + final viewportRect = Rect.fromLTWH(0, scrollController.offset, scrollableSize.width, scrollableSize.height); + final itemVisibility = max(0, tileRect.intersect(viewportRect).height) / tileRect.height; + if (!event.predicate(itemVisibility)) return; + + // most of the time the app bar will be scrolled away after scaling, + // so we compensate for it to center the focal point thumbnail + final appBarHeight = appBarHeightNotifier.value; + final scrollOffset = appBarHeight + tileRect.top + (tileRect.height - scrollableSize.height) * ((event.alignment.y + 1) / 2); + + if (event.animate) { + if (scrollOffset > 0) { + await scrollController.animateTo( + scrollOffset, + duration: Duration(milliseconds: (scrollOffset / 2).round().clamp(Durations.highlightScrollAnimationMinMillis, Durations.highlightScrollAnimationMaxMillis)), + curve: Curves.easeInOutCubic, + ); + } + } else { + final maxScrollExtent = scrollController.position.maxScrollExtent; + scrollController.jumpTo(scrollOffset.clamp(.0, maxScrollExtent)); + await Future.delayed(Durations.highlightJumpDelay); + } + + final highlightItem = event.highlightItem; + if (highlightItem != null) { + context.read().set(highlightItem); + } + } + + @override + void didChangeMetrics() { + final orientation = _windowOrientation; + if (_lastOrientation != orientation) { + _lastOrientation = orientation; + _onWindowOrientationChange(); + } + } + + Future _saveLayoutMetrics() async { + // use a delay to obtain current layout metrics + // so that we can handle window orientation change beforehand with the previous metrics, + // regardless of the `MediaQuery`/`WidgetsBindingObserver` order uncertainty + await Future.delayed(const Duration(milliseconds: 500)); + + if (mounted) { + _lastSectionedListLayout = context.read>(); + _lastScrollableSize = scrollableSize; + } + } + + // the order of `WidgetsBindingObserver` metrics change notification is unreliable + // w.r.t. the `MediaQuery` update, and consequentially to this widget update + // `WidgetsBindingObserver` is notified mostly before, sometimes after, the widget update + void _onWindowOrientationChange() { + final layout = _lastSectionedListLayout; + final halfSize = _lastScrollableSize / 2; + final center = Offset( + halfSize.width, + halfSize.height + scrollController.offset - appBarHeightNotifier.value, + ); + var pivotItem = layout.getItemAt(center) ?? layout.getItemAt(Offset(0, center.dy)); + if (pivotItem == null) { + final pivotSectionKey = layout.getSectionAt(center.dy)?.sectionKey; + if (pivotSectionKey != null) { + pivotItem = layout.sections[pivotSectionKey]?.firstOrNull; + } + } + + if (pivotItem != null) { + WidgetsBinding.instance!.addPostFrameCallback((_) { + context.read().trackItem(pivotItem, animate: false); + }); + } + } +} diff --git a/lib/widgets/common/grid/section_layout.dart b/lib/widgets/common/grid/section_layout.dart index 329ec9c58..4d4d60bc3 100644 --- a/lib/widgets/common/grid/section_layout.dart +++ b/lib/widgets/common/grid/section_layout.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/theme/durations.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -16,13 +17,13 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { final Widget child; const SectionedListLayoutProvider({ - @required this.scrollableWidth, - @required this.columnCount, - this.spacing = 0, - @required this.tileExtent, - @required this.tileBuilder, - this.tileAnimationDelay, - @required this.child, + required this.scrollableWidth, + required this.columnCount, + required this.spacing, + required this.tileExtent, + required this.tileBuilder, + required this.tileAnimationDelay, + required this.child, }) : assert(scrollableWidth != 0); @override @@ -45,9 +46,10 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { final animate = tileAnimationDelay > Duration.zero; final sectionLayouts = []; - var currentIndex = 0, currentOffset = 0.0; + var currentIndex = 0; + var currentOffset = 0.0; sectionKeys.forEach((sectionKey) { - final section = _sections[sectionKey]; + final section = _sections[sectionKey]!; final sectionItemCount = section.length; final rowCount = (sectionItemCount / columnCount).ceil(); final sectionChildCount = 1 + rowCount; @@ -104,7 +106,7 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { bool animate, ) { if (sectionChildIndex == 0) { - final header = headerExtent > 0 ? buildHeader(context, sectionKey, headerExtent) : SizedBox.shrink(); + final header = headerExtent > 0 ? buildHeader(context, sectionKey, headerExtent) : const SizedBox.shrink(); return animate ? _buildAnimation(sectionGridIndex, header) : header; } sectionChildIndex--; @@ -116,12 +118,13 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { final children = []; for (var i = minItemIndex; i < maxItemIndex; i++) { final itemGridIndex = sectionGridIndex + i - minItemIndex; - final item = tileBuilder(section[i]); - if (i != minItemIndex) children.add(SizedBox(width: spacing)); + final item = RepaintBoundary( + child: tileBuilder(section[i]), + ); children.add(animate ? _buildAnimation(itemGridIndex, item) : item); } - return Row( - mainAxisSize: MainAxisSize.min, + return Wrap( + spacing: spacing, children: children, ); } @@ -131,7 +134,7 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { position: index, columnCount: columnCount, duration: Durations.staggeredAnimation, - delay: tileAnimationDelay ?? Durations.staggeredAnimationDelay, + delay: tileAnimationDelay, child: SlideAnimation( verticalOffset: 50.0, child: FadeInAnimation( @@ -168,20 +171,20 @@ class SectionedListLayout { final List sectionLayouts; const SectionedListLayout({ - @required this.sections, - @required this.showHeaders, - @required this.columnCount, - @required this.tileExtent, - @required this.spacing, - @required this.sectionLayouts, + required this.sections, + required this.showHeaders, + required this.columnCount, + required this.tileExtent, + required this.spacing, + required this.sectionLayouts, }); - Rect getTileRect(T item) { - final section = sections.entries.firstWhere((kv) => kv.value.contains(item), orElse: () => null); + Rect? getTileRect(T item) { + final MapEntry>? section = sections.entries.firstWhereOrNull((kv) => kv.value.contains(item)); if (section == null) return null; final sectionKey = section.key; - final sectionLayout = sectionLayouts.firstWhere((sl) => sl.sectionKey == sectionKey, orElse: () => null); + final sectionLayout = sectionLayouts.firstWhereOrNull((sl) => sl.sectionKey == sectionKey); if (sectionLayout == null) return null; final sectionItemIndex = section.value.indexOf(item); @@ -194,9 +197,9 @@ class SectionedListLayout { return Rect.fromLTWH(left, top, tileExtent, tileExtent); } - SectionLayout getSectionAt(double offsetY) => sectionLayouts.firstWhere((sl) => offsetY < sl.maxOffset, orElse: () => null); + SectionLayout? getSectionAt(double offsetY) => sectionLayouts.firstWhereOrNull((sl) => offsetY < sl.maxOffset); - T getItemAt(Offset position) { + T? getItemAt(Offset position) { var dy = position.dy; final sectionLayout = getSectionAt(dy); if (sectionLayout == null) return null; @@ -214,6 +217,9 @@ class SectionedListLayout { return section[index]; } + + @override + String toString() => '$runtimeType#${shortHash(this)}{sectionCount=${sections.length} columnCount=$columnCount, tileExtent=$tileExtent}'; } class SectionLayout { @@ -224,15 +230,15 @@ class SectionLayout { final IndexedWidgetBuilder builder; const SectionLayout({ - @required this.sectionKey, - @required this.firstIndex, - @required this.lastIndex, - @required this.minOffset, - @required this.maxOffset, - @required this.headerExtent, - @required this.tileExtent, - @required this.spacing, - @required this.builder, + required this.sectionKey, + required this.firstIndex, + required this.lastIndex, + required this.minOffset, + required this.maxOffset, + required this.headerExtent, + required this.tileExtent, + required this.spacing, + required this.builder, }) : bodyFirstIndex = firstIndex + 1, bodyMinOffset = minOffset + headerExtent, mainAxisStride = tileExtent + spacing; diff --git a/lib/widgets/common/grid/sliver.dart b/lib/widgets/common/grid/sliver.dart index 1b1ce521f..b54afd3ae 100644 --- a/lib/widgets/common/grid/sliver.dart +++ b/lib/widgets/common/grid/sliver.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; import 'package:aves/widgets/common/grid/section_layout.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -11,9 +12,8 @@ import 'package:provider/provider.dart'; // With the multiple `SliverGrid` solution, thumbnails at the beginning of each sections are built even though they are offscreen // because of `RenderSliverMultiBoxAdaptor.addInitialChild` called by `RenderSliverGrid.performLayout` (line 547), as of Flutter v1.17.0. // cf https://github.com/flutter/flutter/issues/49027 +// adapted from `RenderSliverFixedExtentBoxAdaptor` class SectionedListSliver extends StatelessWidget { - const SectionedListSliver(); - @override Widget build(BuildContext context) { final sectionLayouts = context.watch>().sectionLayouts; @@ -23,11 +23,12 @@ class SectionedListSliver extends StatelessWidget { delegate: SliverChildBuilderDelegate( (context, index) { if (index >= childCount) return null; - final sectionLayout = sectionLayouts.firstWhere((section) => section.hasChild(index), orElse: () => null); - return sectionLayout?.builder(context, index) ?? SizedBox.shrink(); + final sectionLayout = sectionLayouts.firstWhereOrNull((section) => section.hasChild(index)); + return sectionLayout?.builder(context, index) ?? const SizedBox.shrink(); }, childCount: childCount, addAutomaticKeepAlives: false, + addRepaintBoundaries: false, ), ); } @@ -37,9 +38,9 @@ class _SliverKnownExtentList extends SliverMultiBoxAdaptorWidget { final List sectionLayouts; const _SliverKnownExtentList({ - Key key, - @required SliverChildDelegate delegate, - @required this.sectionLayouts, + Key? key, + required SliverChildDelegate delegate, + required this.sectionLayouts, }) : super(key: key, delegate: delegate); @override @@ -60,19 +61,18 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { List get sectionLayouts => _sectionLayouts; set sectionLayouts(List value) { - assert(value != null); if (_sectionLayouts == value) return; _sectionLayouts = value; markNeedsLayout(); } _RenderSliverKnownExtentBoxAdaptor({ - @required RenderSliverBoxChildManager childManager, - @required List sectionLayouts, + required RenderSliverBoxChildManager childManager, + required List sectionLayouts, }) : _sectionLayouts = sectionLayouts, super(childManager: childManager); - SectionLayout sectionAtIndex(int index) => sectionLayouts.firstWhere((section) => section.hasChild(index), orElse: () => null); + SectionLayout? sectionAtIndex(int index) => sectionLayouts.firstWhereOrNull((section) => section.hasChild(index)); SectionLayout sectionAtOffset(double scrollOffset) => sectionLayouts.firstWhere((section) => section.hasChildAtOffset(scrollOffset), orElse: () => sectionLayouts.last); @@ -90,10 +90,10 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { double estimateMaxScrollOffset( SliverConstraints constraints, { - int firstIndex, - int lastIndex, - double leadingScrollOffset, - double trailingScrollOffset, + int? firstIndex, + int? lastIndex, + double? leadingScrollOffset, + double? trailingScrollOffset, }) { return childManager.estimateMaxScrollOffset( constraints, @@ -156,23 +156,11 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { if (firstChild == null) { if (!addInitialChild(index: firstIndex, layoutOffset: indexToLayoutOffset(firstIndex))) { // There are either no children, or we are past the end of all our children. - // If it is the latter, we will need to find the first available child. double max; - if (childManager.childCount != null) { - max = computeMaxScrollOffset(constraints); - } else if (firstIndex <= 0) { + if (firstIndex <= 0) { max = 0.0; } else { - // We will have to find it manually. - var possibleFirstIndex = firstIndex - 1; - while (possibleFirstIndex > 0 && - !addInitialChild( - index: possibleFirstIndex, - layoutOffset: indexToLayoutOffset(possibleFirstIndex), - )) { - possibleFirstIndex -= 1; - } - max = sectionAtIndex(possibleFirstIndex).indexToLayoutOffset(possibleFirstIndex); + max = computeMaxScrollOffset(constraints); } geometry = SliverGeometry( scrollExtent: max, @@ -183,9 +171,9 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { } } - RenderBox trailingChildWithLayout; + RenderBox? trailingChildWithLayout; - for (var index = indexOf(firstChild) - 1; index >= firstIndex; --index) { + for (var index = indexOf(firstChild!) - 1; index >= firstIndex; --index) { final child = insertAndLayoutLeadingChild(childConstraints); if (child == null) { // Items before the previously first child are no longer present. @@ -202,15 +190,15 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { } if (trailingChildWithLayout == null) { - firstChild.layout(childConstraints); - final childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData; + firstChild!.layout(childConstraints); + final childParentData = firstChild!.parentData as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = indexToLayoutOffset(firstIndex); trailingChildWithLayout = firstChild; } var estimatedMaxScrollOffset = double.infinity; - for (var index = indexOf(trailingChildWithLayout) + 1; targetLastIndex == null || index <= targetLastIndex; ++index) { - var child = childAfter(trailingChildWithLayout); + for (var index = indexOf(trailingChildWithLayout!) + 1; targetLastIndex == null || index <= targetLastIndex; ++index) { + var child = childAfter(trailingChildWithLayout!); if (child == null || indexOf(child) != index) { child = insertAndLayoutChild(childConstraints, after: trailingChildWithLayout); if (child == null) { @@ -223,19 +211,18 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor { child.layout(childConstraints); } trailingChildWithLayout = child; - assert(child != null); final childParentData = child.parentData as SliverMultiBoxAdaptorParentData; assert(childParentData.index == index); - childParentData.layoutOffset = indexToLayoutOffset(childParentData.index); + childParentData.layoutOffset = indexToLayoutOffset(childParentData.index!); } - final lastIndex = indexOf(lastChild); + final lastIndex = indexOf(lastChild!); final leadingScrollOffset = indexToLayoutOffset(firstIndex); final trailingScrollOffset = indexToLayoutOffset(lastIndex + 1); - assert(firstIndex == 0 || childScrollOffset(firstChild) - scrollOffset <= precisionErrorTolerance); + assert(firstIndex == 0 || childScrollOffset(firstChild!)! - scrollOffset <= precisionErrorTolerance); assert(debugAssertChildListIsNonEmptyAndContiguous()); - assert(indexOf(firstChild) == firstIndex); + assert(indexOf(firstChild!) == firstIndex); assert(targetLastIndex == null || lastIndex <= targetLastIndex); estimatedMaxScrollOffset = math.min( diff --git a/lib/widgets/common/identity/aves_expansion_tile.dart b/lib/widgets/common/identity/aves_expansion_tile.dart index b4622e6a2..09f6352c9 100644 --- a/lib/widgets/common/identity/aves_expansion_tile.dart +++ b/lib/widgets/common/identity/aves_expansion_tile.dart @@ -4,27 +4,27 @@ import 'package:flutter/material.dart'; class AvesExpansionTile extends StatelessWidget { final String value; - final Widget leading; + final Widget? leading; final String title; - final Color color; - final ValueNotifier expandedNotifier; + final Color? color; + final ValueNotifier? expandedNotifier; final bool initiallyExpanded, showHighlight; final List children; const AvesExpansionTile({ - String value, + String? value, this.leading, - @required this.title, + required this.title, this.color, this.expandedNotifier, this.initiallyExpanded = false, this.showHighlight = true, - @required this.children, - }): value = value ?? title; + required this.children, + }) : value = value ?? title; @override Widget build(BuildContext context) { - final enabled = children?.isNotEmpty == true; + final enabled = children.isNotEmpty == true; Widget titleChild = HighlightTitle( title, color: color, @@ -34,8 +34,8 @@ class AvesExpansionTile extends StatelessWidget { if (leading != null) { titleChild = Row( children: [ - leading, - SizedBox(width: 8), + leading!, + const SizedBox(width: 8), Expanded(child: titleChild), ], ); @@ -52,15 +52,15 @@ class AvesExpansionTile extends StatelessWidget { title: titleChild, expandable: enabled, initiallyExpanded: initiallyExpanded, - finalPadding: EdgeInsets.symmetric(vertical: 6.0), - baseColor: Colors.grey[900], + finalPadding: const EdgeInsets.symmetric(vertical: 6.0), + baseColor: Colors.grey.shade900, expandedColor: Colors.grey[850], shadowColor: Theme.of(context).shadowColor, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Divider(thickness: 1, height: 1), - SizedBox(height: 4), + const Divider(thickness: 1, height: 1), + const SizedBox(height: 4), if (enabled) ...children, ], ), diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 9fe245ac7..24831d348 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -22,13 +22,13 @@ class AvesFilterChip extends StatefulWidget { final CollectionFilter filter; final bool removable; final bool showGenericIcon; - final Widget background; - final Widget details; - final BorderRadius borderRadius; + final Widget? background; + final Widget? details; + final BorderRadius? borderRadius; final double padding; final HeroType heroType; - final FilterCallback onTap; - final OffsetFilterCallback onLongPress; + final FilterCallback? onTap; + final OffsetFilterCallback? onLongPress; static const Color defaultOutlineColor = Colors.white; static const double defaultRadius = 32; @@ -38,8 +38,8 @@ class AvesFilterChip extends StatefulWidget { static const double maxChipWidth = 160; const AvesFilterChip({ - Key key, - @required this.filter, + Key? key, + required this.filter, this.removable = false, this.showGenericIcon = true, this.background, @@ -49,8 +49,7 @@ class AvesFilterChip extends StatefulWidget { this.heroType = HeroType.onTap, this.onTap, this.onLongPress = showDefaultLongPressMenu, - }) : assert(filter != null), - super(key: key); + }) : super(key: key); static Future showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async { if (context.read>().value == AppMode.main) { @@ -65,8 +64,8 @@ class AvesFilterChip extends StatefulWidget { // after the user is done with the popup menu FocusManager.instance.primaryFocus?.unfocus(); - final RenderBox overlay = Overlay.of(context).context.findRenderObject(); - final touchArea = Size(40, 40); + final overlay = Overlay.of(context)!.context.findRenderObject() as RenderBox; + const touchArea = Size(40, 40); final selectedAction = await showMenu( context: context, position: RelativeRect.fromRect(tapPosition & touchArea, Offset.zero & overlay.size), @@ -89,18 +88,18 @@ class AvesFilterChip extends StatefulWidget { } class _AvesFilterChipState extends State { - Future _colorFuture; - Color _outlineColor; - bool _tapped; - Offset _tapPosition; + late Future _colorFuture; + late Color _outlineColor; + late bool _tapped; + Offset? _tapPosition; CollectionFilter get filter => widget.filter; double get padding => widget.padding; - FilterCallback get onTap => widget.onTap; + FilterCallback? get onTap => widget.onTap; - OffsetFilterCallback get onLongPress => widget.onLongPress; + OffsetFilterCallback? get onLongPress => widget.onLongPress; @override void initState() { @@ -171,7 +170,7 @@ class _AvesFilterChipState extends State { mainAxisSize: MainAxisSize.min, children: [ content, - Flexible(child: widget.details), + Flexible(child: widget.details!), ], ); } @@ -186,18 +185,18 @@ class _AvesFilterChipState extends State { child: ColoredBox( color: Colors.black54, child: DefaultTextStyle( - style: Theme.of(context).textTheme.bodyText2.copyWith( - shadows: [Constants.embossShadow], - ), + style: Theme.of(context).textTheme.bodyText2!.copyWith( + shadows: Constants.embossShadows, + ), child: content, ), ), ); } - final borderRadius = widget.borderRadius ?? BorderRadius.circular(AvesFilterChip.defaultRadius); + final borderRadius = widget.borderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius)); Widget chip = Container( - constraints: BoxConstraints( + constraints: const BoxConstraints( minWidth: AvesFilterChip.minChipWidth, maxWidth: AvesFilterChip.maxChipWidth, minHeight: AvesFilterChip.minChipHeight, @@ -224,29 +223,29 @@ 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, - onLongPress: onLongPress != null ? () => onLongPress(context, filter, _tapPosition) : null, + onLongPress: onLongPress != null ? () => onLongPress!(context, filter, _tapPosition!) : null, borderRadius: borderRadius, child: FutureBuilder( future: _colorFuture, builder: (context, snapshot) { if (snapshot.hasData) { - _outlineColor = snapshot.data; + _outlineColor = snapshot.data!; } return DecoratedBox( decoration: BoxDecoration( - border: Border.all( + border: Border.fromBorderSide(BorderSide( color: _outlineColor, width: AvesFilterChip.outlineWidth, - ), + )), borderRadius: borderRadius, ), position: DecorationPosition.foreground, child: Padding( - padding: EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 8), child: content, ), ); @@ -264,7 +263,7 @@ class _AvesFilterChipState extends State { tag: filter, transitionOnUserGestures: true, child: DefaultTextStyle( - style: TextStyle(), + style: const TextStyle(), child: chip, ), ); diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index bec2db615..433764ab3 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -14,8 +14,8 @@ class VideoIcon extends StatelessWidget { final AvesEntry entry; const VideoIcon({ - Key key, - this.entry, + Key? key, + required this.entry, }) : super(key: key); @override @@ -31,7 +31,7 @@ class VideoIcon extends StatelessWidget { if (showDuration) { child = DefaultTextStyle( style: TextStyle( - color: Colors.grey[200], + color: Colors.grey.shade200, fontSize: thumbnailTheme.fontSize, ), child: child, @@ -42,7 +42,7 @@ class VideoIcon extends StatelessWidget { } class AnimatedImageIcon extends StatelessWidget { - const AnimatedImageIcon({Key key}) : super(key: key); + const AnimatedImageIcon({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -55,7 +55,7 @@ class AnimatedImageIcon extends StatelessWidget { } class GeotiffIcon extends StatelessWidget { - const GeotiffIcon({Key key}) : super(key: key); + const GeotiffIcon({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -67,7 +67,7 @@ class GeotiffIcon extends StatelessWidget { } class SphericalImageIcon extends StatelessWidget { - const SphericalImageIcon({Key key}) : super(key: key); + const SphericalImageIcon({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -79,7 +79,7 @@ class SphericalImageIcon extends StatelessWidget { } class GpsIcon extends StatelessWidget { - const GpsIcon({Key key}) : super(key: key); + const GpsIcon({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -91,7 +91,7 @@ class GpsIcon extends StatelessWidget { } class RawIcon extends StatelessWidget { - const RawIcon({Key key}) : super(key: key); + const RawIcon({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -106,8 +106,8 @@ class MultiPageIcon extends StatelessWidget { final AvesEntry entry; const MultiPageIcon({ - Key key, - this.entry, + Key? key, + required this.entry, }) : super(key: key); @override @@ -123,13 +123,13 @@ class MultiPageIcon extends StatelessWidget { class OverlayIcon extends StatelessWidget { final IconData icon; final double size; - final String text; + final String? text; final double iconScale; const OverlayIcon({ - Key key, - @required this.icon, - @required this.size, + Key? key, + required this.icon, + required this.size, this.iconScale = 1, this.text, }) : super(key: key); @@ -150,11 +150,11 @@ class OverlayIcon extends StatelessWidget { ); return Container( - margin: EdgeInsets.all(1), + margin: const EdgeInsets.all(1), padding: text != null ? EdgeInsets.only(right: size / 4) : null, decoration: BoxDecoration( - color: Color(0xBB000000), - borderRadius: BorderRadius.circular(size), + color: const Color(0xBB000000), + borderRadius: BorderRadius.all(Radius.circular(size)), ), child: text == null ? iconBox @@ -163,8 +163,8 @@ class OverlayIcon extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ iconBox, - SizedBox(width: 2), - Text(text), + const SizedBox(width: 2), + Text(text!), ], ), ); @@ -172,10 +172,10 @@ class OverlayIcon extends StatelessWidget { } class IconUtils { - static Widget getAlbumIcon({ - @required BuildContext context, - @required String album, - double size, + static Widget? getAlbumIcon({ + required BuildContext context, + required String albumPath, + double? size, bool embossed = false, }) { size ??= IconTheme.of(context).size; @@ -187,7 +187,7 @@ class IconUtils { data: context.read().copyWith(textScaleFactor: 1.0), child: DecoratedIcon( icon, - shadows: [Constants.embossShadow], + shadows: Constants.embossShadows, size: size, ), ) @@ -195,7 +195,7 @@ class IconUtils { icon, size: size, ); - switch (androidFileUtils.getAlbumType(album)) { + switch (androidFileUtils.getAlbumType(albumPath)) { case AlbumType.camera: return buildIcon(AIcons.cameraAlbum); case AlbumType.screenshots: @@ -206,8 +206,8 @@ class IconUtils { case AlbumType.app: return Image( image: AppIconImage( - packageName: androidFileUtils.getAlbumAppPackageName(album), - size: size, + packageName: androidFileUtils.getAlbumAppPackageName(albumPath)!, + size: size!, ), width: size, height: size, diff --git a/lib/widgets/common/identity/aves_logo.dart b/lib/widgets/common/identity/aves_logo.dart index a458013e9..89ded5caa 100644 --- a/lib/widgets/common/identity/aves_logo.dart +++ b/lib/widgets/common/identity/aves_logo.dart @@ -4,7 +4,7 @@ import 'package:flutter_svg/flutter_svg.dart'; class AvesLogo extends StatelessWidget { final double size; - const AvesLogo({@required this.size}); + const AvesLogo({required this.size}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/identity/empty.dart b/lib/widgets/common/identity/empty.dart index 6bc02eea0..27e5ac597 100644 --- a/lib/widgets/common/identity/empty.dart +++ b/lib/widgets/common/identity/empty.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; class EmptyContent extends StatelessWidget { - final IconData icon; + final IconData? icon; final String text; final AlignmentGeometry alignment; const EmptyContent({ this.icon, - @required this.text, + required this.text, this.alignment = const FractionalOffset(.5, .35), }); @@ -25,11 +25,11 @@ class EmptyContent extends StatelessWidget { size: 64, color: color, ), - SizedBox(height: 16) + const SizedBox(height: 16) ], Text( text, - style: TextStyle( + style: const TextStyle( color: color, fontSize: 22, ), diff --git a/lib/widgets/common/identity/highlight_title.dart b/lib/widgets/common/identity/highlight_title.dart index c36ff5ffc..8d9c95a93 100644 --- a/lib/widgets/common/identity/highlight_title.dart +++ b/lib/widgets/common/identity/highlight_title.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; class HighlightTitle extends StatelessWidget { final String title; - final Color color; + final Color? color; final double fontSize; final bool enabled, selectable; final bool showHighlight; @@ -18,14 +18,14 @@ class HighlightTitle extends StatelessWidget { this.enabled = true, this.selectable = false, this.showHighlight = true, - }) : assert(title != null); + }); static const disabledColor = Colors.grey; @override Widget build(BuildContext context) { final style = TextStyle( - shadows: [ + shadows: const [ Shadow( color: Colors.black, offset: Offset(1, 1), @@ -34,7 +34,7 @@ class HighlightTitle extends StatelessWidget { ], fontSize: fontSize, letterSpacing: 1.0, - fontFeatures: [FontFeature.enable('smcp')], + fontFeatures: const [FontFeature.enable('smcp')], ); return Align( @@ -45,7 +45,7 @@ class HighlightTitle extends StatelessWidget { color: enabled ? color ?? stringToColor(title) : disabledColor, ) : null, - margin: EdgeInsets.symmetric(vertical: 4.0), + margin: const EdgeInsets.symmetric(vertical: 4.0), child: selectable ? SelectableText( title, diff --git a/lib/widgets/common/identity/scroll_thumb.dart b/lib/widgets/common/identity/scroll_thumb.dart index ed0ee3ec1..b79cc1fb7 100644 --- a/lib/widgets/common/identity/scroll_thumb.dart +++ b/lib/widgets/common/identity/scroll_thumb.dart @@ -6,24 +6,24 @@ const double avesScrollThumbHeight = 48; // height and background color do not change // so we do not rely on the builder props ScrollThumbBuilder avesScrollThumbBuilder({ - @required double height, - @required Color backgroundColor, + required double height, + required Color backgroundColor, }) { final scrollThumb = Container( - decoration: BoxDecoration( + decoration: const BoxDecoration( color: Colors.black26, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.all(Radius.circular(12)), ), height: height, - margin: EdgeInsets.only(right: .5), - padding: EdgeInsets.all(2), + margin: const EdgeInsets.only(right: .5), + padding: const EdgeInsets.all(2), child: ClipPath( clipper: ArrowClipper(), child: Container( width: 20.0, decoration: BoxDecoration( color: backgroundColor, - borderRadius: BorderRadius.circular(12), + borderRadius: const BorderRadius.all(Radius.circular(12)), ), ), ), diff --git a/lib/widgets/common/magnifier/controller/controller.dart b/lib/widgets/common/magnifier/controller/controller.dart index d4c5b37f9..27513bb9a 100644 --- a/lib/widgets/common/magnifier/controller/controller.dart +++ b/lib/widgets/common/magnifier/controller/controller.dart @@ -12,9 +12,9 @@ class MagnifierController { final StreamController _scaleBoundariesStreamController = StreamController.broadcast(); final StreamController _scaleStateChangeStreamController = StreamController.broadcast(); - MagnifierState _currentState, initial, previousState; - ScaleBoundaries _scaleBoundaries; - ScaleStateChange _currentScaleState, previousScaleState; + late MagnifierState _currentState, initial, previousState; + ScaleBoundaries? _scaleBoundaries; + late ScaleStateChange _currentScaleState, previousScaleState; MagnifierController({ Offset initialPosition = Offset.zero, @@ -25,10 +25,12 @@ class MagnifierController { source: ChangeSource.internal, ); previousState = initial; + _currentState = initial; _setState(initial); - final _initialScaleState = ScaleStateChange(state: ScaleState.initial, source: ChangeSource.internal); + const _initialScaleState = ScaleStateChange(state: ScaleState.initial, source: ChangeSource.internal); previousScaleState = _initialScaleState; + _currentScaleState = _initialScaleState; _setScaleState(_initialScaleState); } @@ -42,9 +44,9 @@ class MagnifierController { Offset get position => currentState.position; - double get scale => currentState.scale; + double? get scale => currentState.scale; - ScaleBoundaries get scaleBoundaries => _scaleBoundaries; + ScaleBoundaries get scaleBoundaries => _scaleBoundaries!; ScaleStateChange get scaleState => _currentScaleState; @@ -60,9 +62,9 @@ class MagnifierController { } void update({ - Offset position, - double scale, - @required ChangeSource source, + Offset? position, + double? scale, + required ChangeSource source, }) { position = position ?? this.position; scale = scale ?? this.scale; @@ -76,7 +78,7 @@ class MagnifierController { )); } - void setScaleState(ScaleState newValue, ChangeSource source, {Offset childFocalPoint}) { + void setScaleState(ScaleState newValue, ChangeSource source, {Offset? childFocalPoint}) { if (_currentScaleState.state == newValue) return; previousScaleState = _currentScaleState; @@ -109,18 +111,18 @@ class MagnifierController { _scaleStateChangeStreamController.sink.add(_currentScaleState); } - double getScaleForScaleState(ScaleState scaleState) { - double _clamp(double scale, ScaleBoundaries boundaries) => scale.clamp(boundaries.minScale, boundaries.maxScale); + double? getScaleForScaleState(ScaleState scaleState) { + double _clamp(double scale) => scale.clamp(scaleBoundaries.minScale, scaleBoundaries.maxScale); switch (scaleState) { case ScaleState.initial: case ScaleState.zoomedIn: case ScaleState.zoomedOut: - return _clamp(scaleBoundaries.initialScale, scaleBoundaries); + return _clamp(scaleBoundaries.initialScale); case ScaleState.covering: - return _clamp(ScaleLevel.scaleForCovering(scaleBoundaries.viewportSize, scaleBoundaries.childSize), scaleBoundaries); + return _clamp(ScaleLevel.scaleForCovering(scaleBoundaries.viewportSize, scaleBoundaries.childSize)); case ScaleState.originalSize: - return _clamp(1.0, scaleBoundaries); + return _clamp(1.0); default: return null; } diff --git a/lib/widgets/common/magnifier/controller/controller_delegate.dart b/lib/widgets/common/magnifier/controller/controller_delegate.dart index 4169dced5..a6a79b3d7 100644 --- a/lib/widgets/common/magnifier/controller/controller_delegate.dart +++ b/lib/widgets/common/magnifier/controller/controller_delegate.dart @@ -16,11 +16,15 @@ mixin MagnifierControllerDelegate on State { ScaleBoundaries get scaleBoundaries => controller.scaleBoundaries; + Size get childSize => scaleBoundaries.childSize; + + Size get viewportSize => scaleBoundaries.viewportSize; + ScaleStateCycle get scaleStateCycle => widget.scaleStateCycle; Alignment get basePosition => Alignment.center; - Function(double prevScale, double nextScale, Offset nextPosition) _animateScale; + Function(double? prevScale, double? nextScale, Offset nextPosition)? _animateScale; /// Mark if scale need recalculation, useful for scale boundaries changes. bool markNeedsScaleRecalc = true; @@ -47,15 +51,15 @@ mixin MagnifierControllerDelegate on State { if (nextScaleState == ScaleState.covering || nextScaleState == ScaleState.originalSize) { final childFocalPoint = scaleStateChange.childFocalPoint; if (childFocalPoint != null) { - nextPosition = scaleBoundaries.childToStatePosition(nextScale, childFocalPoint); + nextPosition = scaleBoundaries.childToStatePosition(nextScale!, childFocalPoint); } } final prevScale = controller.scale ?? controller.getScaleForScaleState(controller.previousScaleState.state); - _animateScale(prevScale, nextScale, nextPosition); + _animateScale!(prevScale, nextScale, nextPosition); } - void setScaleStateUpdateAnimation(void Function(double prevScale, double nextScale, Offset nextPosition) animateScale) { + void setScaleStateUpdateAnimation(void Function(double? prevScale, double? nextScale, Offset nextPosition) animateScale) { _animateScale = animateScale; } @@ -64,13 +68,13 @@ mixin MagnifierControllerDelegate on State { if (controller.scale == controller.previousState.scale) return; if (state.source == ChangeSource.internal || state.source == ChangeSource.animation) return; - final newScaleState = (scale > scaleBoundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut; + final newScaleState = (scale! > scaleBoundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut; controller.setScaleState(newScaleState, state.source); } Offset get position => controller.position; - double get scale { + double? get scale { final scaleState = controller.scaleState.state; final needsRecalc = markNeedsScaleRecalc && !(scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut); final scaleExistsOnController = controller.scale != null; @@ -83,12 +87,12 @@ mixin MagnifierControllerDelegate on State { return controller.scale; } - void setScale(double scale, ChangeSource source) => controller.update(scale: scale, source: source); + void setScale(double? scale, ChangeSource source) => controller.update(scale: scale, source: source); void updateMultiple({ - @required Offset position, - @required double scale, - @required ChangeSource source, + required Offset position, + required double scale, + required ChangeSource source, }) { controller.update(position: position, scale: scale, source: source); } @@ -101,7 +105,7 @@ mixin MagnifierControllerDelegate on State { controller.setScaleState(newScaleState, source); } - void nextScaleState(ChangeSource source, {Offset childFocalPoint}) { + void nextScaleState(ChangeSource source, {Offset? childFocalPoint}) { final scaleState = controller.scaleState.state; if (scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut) { controller.setScaleState(scaleStateCycle(scaleState), source, childFocalPoint: childFocalPoint); @@ -125,11 +129,11 @@ mixin MagnifierControllerDelegate on State { controller.setScaleState(nextScaleState, source, childFocalPoint: childFocalPoint); } - CornersRange cornersX({double scale}) { - final _scale = scale ?? this.scale; + CornersRange cornersX({double? scale}) { + final _scale = scale ?? this.scale!; - final computedWidth = scaleBoundaries.childSize.width * _scale; - final screenWidth = scaleBoundaries.viewportSize.width; + final computedWidth = childSize.width * _scale; + final screenWidth = viewportSize.width; final positionX = basePosition.x; final widthDiff = computedWidth - screenWidth; @@ -139,11 +143,11 @@ mixin MagnifierControllerDelegate on State { return CornersRange(minX, maxX); } - CornersRange cornersY({double scale}) { - final _scale = scale ?? this.scale; + CornersRange cornersY({double? scale}) { + final _scale = scale ?? this.scale!; - final computedHeight = scaleBoundaries.childSize.height * _scale; - final screenHeight = scaleBoundaries.viewportSize.height; + final computedHeight = childSize.height * _scale; + final screenHeight = viewportSize.height; final positionY = basePosition.y; final heightDiff = computedHeight - screenHeight; @@ -153,15 +157,15 @@ mixin MagnifierControllerDelegate on State { return CornersRange(minY, maxY); } - Offset clampPosition({Offset position, double scale}) { - final _scale = scale ?? this.scale; + Offset clampPosition({Offset? position, double? scale}) { + final _scale = scale ?? this.scale!; final _position = position ?? this.position; - final computedWidth = scaleBoundaries.childSize.width * _scale; - final computedHeight = scaleBoundaries.childSize.height * _scale; + final computedWidth = childSize.width * _scale; + final computedHeight = childSize.height * _scale; - final screenWidth = scaleBoundaries.viewportSize.width; - final screenHeight = scaleBoundaries.viewportSize.height; + final screenWidth = viewportSize.width; + final screenHeight = viewportSize.height; var finalX = 0.0; if (screenWidth < computedWidth) { diff --git a/lib/widgets/common/magnifier/controller/state.dart b/lib/widgets/common/magnifier/controller/state.dart index 6185a1707..949c692c0 100644 --- a/lib/widgets/common/magnifier/controller/state.dart +++ b/lib/widgets/common/magnifier/controller/state.dart @@ -6,13 +6,13 @@ import 'package:flutter/widgets.dart'; @immutable class MagnifierState { const MagnifierState({ - @required this.position, - @required this.scale, - @required this.source, + required this.position, + required this.scale, + required this.source, }); final Offset position; - final double scale; + final double? scale; final ChangeSource source; @override diff --git a/lib/widgets/common/magnifier/core/core.dart b/lib/widgets/common/magnifier/core/core.dart index 1f81097e5..7ea5035e3 100644 --- a/lib/widgets/common/magnifier/core/core.dart +++ b/lib/widgets/common/magnifier/core/core.dart @@ -12,12 +12,12 @@ import 'package:flutter/widgets.dart'; /// to user gestures, updates to the controller state and mounts the entire Layout class MagnifierCore extends StatefulWidget { const MagnifierCore({ - Key key, - @required this.child, - @required this.onTap, - @required this.controller, - @required this.scaleStateCycle, - @required this.applyScale, + Key? key, + required this.child, + required this.onTap, + required this.controller, + required this.scaleStateCycle, + required this.applyScale, this.panInertia = .2, }) : super(key: key); @@ -26,7 +26,7 @@ class MagnifierCore extends StatefulWidget { final MagnifierController controller; final ScaleStateCycle scaleStateCycle; - final MagnifierTapCallback onTap; + final MagnifierTapCallback? onTap; final bool applyScale; final double panInertia; @@ -38,18 +38,18 @@ class MagnifierCore extends StatefulWidget { } class _MagnifierCoreState extends State with TickerProviderStateMixin, MagnifierControllerDelegate, CornerHitDetector { - Offset _startFocalPoint, _lastViewportFocalPosition; - double _startScale, _quickScaleLastY, _quickScaleLastDistance; - bool _doubleTap, _quickScaleMoved; + Offset? _startFocalPoint, _lastViewportFocalPosition; + double? _startScale, _quickScaleLastY, _quickScaleLastDistance; + late bool _doubleTap, _quickScaleMoved; DateTime _lastScaleGestureDate = DateTime.now(); - AnimationController _scaleAnimationController; - Animation _scaleAnimation; + late AnimationController _scaleAnimationController; + late Animation _scaleAnimation; - AnimationController _positionAnimationController; - Animation _positionAnimation; + late AnimationController _positionAnimationController; + late Animation _positionAnimation; - ScaleBoundaries cachedScaleBoundaries; + ScaleBoundaries? cachedScaleBoundaries; void handleScaleAnimation() { setScale(_scaleAnimation.value, ChangeSource.animation); @@ -65,7 +65,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM _lastViewportFocalPosition = _startFocalPoint; _doubleTap = doubleTap; _quickScaleLastDistance = null; - _quickScaleLastY = _startFocalPoint.dy; + _quickScaleLastY = _startFocalPoint!.dy; _quickScaleMoved = false; _scaleAnimationController.stop(); @@ -78,21 +78,21 @@ class _MagnifierCoreState extends State with TickerProviderStateM // quick scale, aka one finger zoom // magic numbers from `davemorrissey/subsampling-scale-image-view` final focalPointY = details.focalPoint.dy; - final distance = (focalPointY - _startFocalPoint.dy).abs() * 2 + 20; + final distance = (focalPointY - _startFocalPoint!.dy).abs() * 2 + 20; _quickScaleLastDistance ??= distance; - final spanDiff = (1 - (distance / _quickScaleLastDistance)).abs() * .5; + final spanDiff = (1 - (distance / _quickScaleLastDistance!)).abs() * .5; _quickScaleMoved |= spanDiff > .03; - final factor = _quickScaleMoved ? (focalPointY > _quickScaleLastY ? (1 + spanDiff) : (1 - spanDiff)) : 1; + final factor = _quickScaleMoved ? (focalPointY > _quickScaleLastY! ? (1 + spanDiff) : (1 - spanDiff)) : 1; _quickScaleLastDistance = distance; _quickScaleLastY = focalPointY; - newScale = scale * factor; + newScale = scale! * factor; } else { - newScale = _startScale * details.scale; + newScale = _startScale! * details.scale; } - final scaleFocalPoint = _doubleTap ? _startFocalPoint : details.focalPoint; + final scaleFocalPoint = _doubleTap ? _startFocalPoint! : details.focalPoint; - final panPositionDelta = scaleFocalPoint - _lastViewportFocalPosition; - final scalePositionDelta = scaleBoundaries.viewportToStatePosition(controller, scaleFocalPoint) * (scale / newScale - 1); + final panPositionDelta = scaleFocalPoint - _lastViewportFocalPosition!; + final scalePositionDelta = scaleBoundaries.viewportToStatePosition(controller, scaleFocalPoint) * (scale! / newScale - 1); final newPosition = position + panPositionDelta + scalePositionDelta; updateScaleStateFromNewScale(newScale, ChangeSource.gesture); @@ -107,7 +107,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM void onScaleEnd(ScaleEndDetails details) { final _position = controller.position; - final _scale = controller.scale; + final _scale = controller.scale!; final maxScale = scaleBoundaries.maxScale; final minScale = scaleBoundaries.minScale; @@ -145,14 +145,14 @@ class _MagnifierCoreState extends State with TickerProviderStateM } Duration _getAnimationDurationForVelocity({ - Cubic curve, - Tween tween, - Offset targetPixelPerSecond, + required Cubic curve, + required Tween tween, + required Offset targetPixelPerSecond, }) { assert(targetPixelPerSecond != Offset.zero); // find initial animation velocity over the first 20% of the specified curve const t = 0.2; - final animationVelocity = (tween.end - tween.begin).distance * curve.transform(t) / t; + final animationVelocity = (tween.end! - tween.begin!).distance * curve.transform(t) / t; final gestureVelocity = targetPixelPerSecond.distance; return Duration(milliseconds: (animationVelocity / gestureVelocity * 1000).round()); } @@ -162,16 +162,16 @@ class _MagnifierCoreState extends State with TickerProviderStateM final viewportTapPosition = details.localPosition; final childTapPosition = scaleBoundaries.viewportToChildPosition(controller, viewportTapPosition); - widget.onTap.call(context, details, controller.currentState, childTapPosition); + widget.onTap!.call(context, details, controller.currentState, childTapPosition); } void onDoubleTap(TapDownDetails details) { - final viewportTapPosition = details?.localPosition; + final viewportTapPosition = details.localPosition; final childTapPosition = scaleBoundaries.viewportToChildPosition(controller, viewportTapPosition); nextScaleState(ChangeSource.gesture, childFocalPoint: childTapPosition); } - void animateScale(double from, double to) { + void animateScale(double? from, double? to) { _scaleAnimation = Tween( begin: from, end: to, @@ -215,7 +215,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM cachedScaleBoundaries = widget.controller.scaleBoundaries; } - void animateOnScaleStateUpdate(double prevScale, double nextScale, Offset nextPosition) { + void animateOnScaleStateUpdate(double? prevScale, double? nextScale, Offset nextPosition) { animateScale(prevScale, nextScale); animatePosition(controller.position, nextPosition); } @@ -242,7 +242,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM builder: (context, snapshot) { if (!snapshot.hasData) return Container(); - final magnifierState = snapshot.data; + final magnifierState = snapshot.data!; final position = magnifierState.position; final applyScale = widget.applyScale; @@ -302,7 +302,7 @@ class _CenterWithOriginalSizeDelegate extends SingleChildLayoutDelegate { @override BoxConstraints getConstraintsForChild(BoxConstraints constraints) { - return applyScale ? BoxConstraints.tight(subjectSize) : BoxConstraints(); + return applyScale ? BoxConstraints.tight(subjectSize) : const BoxConstraints(); } @override diff --git a/lib/widgets/common/magnifier/core/gesture_detector.dart b/lib/widgets/common/magnifier/core/gesture_detector.dart index 84ccf9c33..ca1e20720 100644 --- a/lib/widgets/common/magnifier/core/gesture_detector.dart +++ b/lib/widgets/common/magnifier/core/gesture_detector.dart @@ -7,8 +7,8 @@ import '../pan/corner_hit_detector.dart'; class MagnifierGestureDetector extends StatefulWidget { const MagnifierGestureDetector({ - Key key, - this.hitDetector, + Key? key, + required this.hitDetector, this.onScaleStart, this.onScaleUpdate, this.onScaleEnd, @@ -20,31 +20,26 @@ class MagnifierGestureDetector extends StatefulWidget { }) : super(key: key); final CornerHitDetector hitDetector; - final void Function(ScaleStartDetails details, bool doubleTap) onScaleStart; - final GestureScaleUpdateCallback onScaleUpdate; - final GestureScaleEndCallback onScaleEnd; + final void Function(ScaleStartDetails details, bool doubleTap)? onScaleStart; + final GestureScaleUpdateCallback? onScaleUpdate; + final GestureScaleEndCallback? onScaleEnd; - final GestureTapDownCallback onTapDown; - final GestureTapUpCallback onTapUp; - final GestureTapDownCallback onDoubleTap; + final GestureTapDownCallback? onTapDown; + final GestureTapUpCallback? onTapUp; + final GestureTapDownCallback? onDoubleTap; - final HitTestBehavior behavior; - final Widget child; + final HitTestBehavior? behavior; + final Widget? child; @override _MagnifierGestureDetectorState createState() => _MagnifierGestureDetectorState(); } class _MagnifierGestureDetectorState extends State { - final ValueNotifier doubleTapDetails = ValueNotifier(null); + final ValueNotifier doubleTapDetails = ValueNotifier(null); @override Widget build(BuildContext context) { - final scope = MagnifierGestureDetectorScope.of(context); - - final axis = scope?.axis; - final touchSlopFactor = scope?.touchSlopFactor; - final gestures = {}; if (widget.onTapDown != null || widget.onTapUp != null) { @@ -58,30 +53,35 @@ class _MagnifierGestureDetectorState extends State { ); } - gestures[MagnifierGestureRecognizer] = GestureRecognizerFactoryWithHandlers( - () => MagnifierGestureRecognizer( - hitDetector: widget.hitDetector, - debugOwner: this, - validateAxis: axis, - touchSlopFactor: touchSlopFactor, - doubleTapDetails: doubleTapDetails, - ), - (instance) { - instance.onStart = (details) => widget.onScaleStart(details, doubleTapDetails.value != null); - instance.onUpdate = widget.onScaleUpdate; - instance.onEnd = widget.onScaleEnd; - }, - ); + final scope = MagnifierGestureDetectorScope.of(context); + if (scope != null) { + gestures[MagnifierGestureRecognizer] = GestureRecognizerFactoryWithHandlers( + () => MagnifierGestureRecognizer( + hitDetector: widget.hitDetector, + debugOwner: this, + validateAxis: scope.axis, + touchSlopFactor: scope.touchSlopFactor, + doubleTapDetails: doubleTapDetails, + ), + (instance) { + instance.onStart = widget.onScaleStart != null ? (details) => widget.onScaleStart!(details, doubleTapDetails.value != null) : null; + instance.onUpdate = widget.onScaleUpdate; + instance.onEnd = widget.onScaleEnd; + }, + ); + } gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => DoubleTapGestureRecognizer(debugOwner: this), (instance) { instance.onDoubleTapCancel = () => doubleTapDetails.value = null; instance.onDoubleTapDown = (details) => doubleTapDetails.value = details; - instance.onDoubleTap = () { - widget.onDoubleTap(doubleTapDetails.value); - doubleTapDetails.value = null; - }; + instance.onDoubleTap = widget.onDoubleTap != null + ? () { + widget.onDoubleTap!(doubleTapDetails.value!); + doubleTapDetails.value = null; + } + : null; }, ); diff --git a/lib/widgets/common/magnifier/core/scale_gesture_recognizer.dart b/lib/widgets/common/magnifier/core/scale_gesture_recognizer.dart index 37db8bbc6..b95d73f72 100644 --- a/lib/widgets/common/magnifier/core/scale_gesture_recognizer.dart +++ b/lib/widgets/common/magnifier/core/scale_gesture_recognizer.dart @@ -9,23 +9,23 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { final CornerHitDetector hitDetector; final List validateAxis; final double touchSlopFactor; - final ValueNotifier doubleTapDetails; + final ValueNotifier doubleTapDetails; MagnifierGestureRecognizer({ - Object debugOwner, - PointerDeviceKind kind, - this.hitDetector, - this.validateAxis, + Object? debugOwner, + PointerDeviceKind? kind, + required this.hitDetector, + required this.validateAxis, this.touchSlopFactor = 2, - this.doubleTapDetails, + required this.doubleTapDetails, }) : super(debugOwner: debugOwner, kind: kind); Map _pointerLocations = {}; - Offset _initialFocalPoint; - Offset _currentFocalPoint; - double _initialSpan; - double _currentSpan; + Offset? _initialFocalPoint; + Offset? _currentFocalPoint; + double? _initialSpan; + double? _currentSpan; bool ready = true; @@ -48,7 +48,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { @override void handleEvent(PointerEvent event) { - if (validateAxis != null && validateAxis.isNotEmpty) { + if (validateAxis.isNotEmpty) { var didChangeConfiguration = false; if (event is PointerMoveEvent) { if (!event.synthesized) { @@ -82,7 +82,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { // Compute the focal point var focalPoint = Offset.zero; for (final pointer in _pointerLocations.keys) { - focalPoint += _pointerLocations[pointer]; + focalPoint += _pointerLocations[pointer]!; } _currentFocalPoint = count > 0 ? focalPoint / count.toDouble() : Offset.zero; @@ -91,7 +91,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { // vertical coordinates, respectively. var totalDeviation = 0.0; for (final pointer in _pointerLocations.keys) { - totalDeviation += (_currentFocalPoint - _pointerLocations[pointer]).distance; + totalDeviation += (_currentFocalPoint! - _pointerLocations[pointer]!).distance; } _currentSpan = count > 0 ? totalDeviation / count : 0.0; } @@ -106,7 +106,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { return; } - final move = _initialFocalPoint - _currentFocalPoint; + final move = _initialFocalPoint! - _currentFocalPoint!; var shouldMove = false; if (validateAxis.length == 2) { // the image is the descendant of gesture detector(s) handling drag in both directions @@ -128,10 +128,10 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer { shouldMove = validateAxis.contains(Axis.vertical) ? hitDetector.shouldMoveY(move) : hitDetector.shouldMoveX(move); } - final doubleTap = doubleTapDetails?.value != null; + final doubleTap = doubleTapDetails.value != null; if (shouldMove || doubleTap) { - final spanDelta = (_currentSpan - _initialSpan).abs(); - final focalPointDelta = (_currentFocalPoint - _initialFocalPoint).distance; + final spanDelta = (_currentSpan! - _initialSpan!).abs(); + final focalPointDelta = (_currentFocalPoint! - _initialFocalPoint!).distance; // warning: do not compare `focalPointDelta` to `kPanSlop` // `ScaleGestureRecognizer` uses `kPanSlop`, but `HorizontalDragGestureRecognizer` uses `kTouchSlop` // and the magnifier recognizer may compete with the `HorizontalDragGestureRecognizer` from a containing `PageView` diff --git a/lib/widgets/common/magnifier/magnifier.dart b/lib/widgets/common/magnifier/magnifier.dart index 44fa6f590..17091e4d5 100644 --- a/lib/widgets/common/magnifier/magnifier.dart +++ b/lib/widgets/common/magnifier/magnifier.dart @@ -20,24 +20,17 @@ import 'package:flutter/material.dart'; */ class Magnifier extends StatelessWidget { const Magnifier({ - Key key, - @required this.controller, - @required this.childSize, + Key? key, + required this.controller, + required this.childSize, this.minScale = const ScaleLevel(factor: .0), this.maxScale = const ScaleLevel(factor: double.infinity), this.initialScale = const ScaleLevel(ref: ScaleReference.contained), this.scaleStateCycle = defaultScaleStateCycle, this.applyScale = true, this.onTap, - @required this.child, - }) : assert(controller != null), - assert(childSize != null), - assert(minScale != null), - assert(maxScale != null), - assert(initialScale != null), - assert(scaleStateCycle != null), - assert(applyScale != null), - super(key: key); + required this.child, + }) : super(key: key); final MagnifierController controller; @@ -55,7 +48,7 @@ class Magnifier extends StatelessWidget { final ScaleStateCycle scaleStateCycle; final bool applyScale; - final MagnifierTapCallback onTap; + final MagnifierTapCallback? onTap; final Widget child; @override @@ -67,7 +60,7 @@ class Magnifier extends StatelessWidget { maxScale: maxScale, initialScale: initialScale, viewportSize: constraints.biggest, - childSize: childSize?.isEmpty == false ? childSize : constraints.biggest, + childSize: childSize.isEmpty == false ? childSize : constraints.biggest, )); return MagnifierCore( diff --git a/lib/widgets/common/magnifier/pan/corner_hit_detector.dart b/lib/widgets/common/magnifier/pan/corner_hit_detector.dart index 482b39f5b..604666b7e 100644 --- a/lib/widgets/common/magnifier/pan/corner_hit_detector.dart +++ b/lib/widgets/common/magnifier/pan/corner_hit_detector.dart @@ -11,10 +11,10 @@ mixin CornerHitDetector on MagnifierControllerDelegate { // so be sure to compare with `precisionErrorTolerance` _CornerHit _hitCornersX() { - final childWidth = scaleBoundaries.childSize.width * scale; + final childWidth = scaleBoundaries.childSize.width * scale!; final viewportWidth = scaleBoundaries.viewportSize.width; if (viewportWidth + precisionErrorTolerance >= childWidth) { - return _CornerHit(true, true); + return const _CornerHit(true, true); } final x = -position.dx; final cornersX = this.cornersX(); @@ -22,10 +22,10 @@ mixin CornerHitDetector on MagnifierControllerDelegate { } _CornerHit _hitCornersY() { - final childHeight = scaleBoundaries.childSize.height * scale; + final childHeight = scaleBoundaries.childSize.height * scale!; final viewportHeight = scaleBoundaries.viewportSize.height; if (viewportHeight + precisionErrorTolerance >= childHeight) { - return _CornerHit(true, true); + return const _CornerHit(true, true); } final y = -position.dy; final cornersY = this.cornersY(); diff --git a/lib/widgets/common/magnifier/pan/gesture_detector_scope.dart b/lib/widgets/common/magnifier/pan/gesture_detector_scope.dart index 8eaee4f69..e135cf181 100644 --- a/lib/widgets/common/magnifier/pan/gesture_detector_scope.dart +++ b/lib/widgets/common/magnifier/pan/gesture_detector_scope.dart @@ -8,12 +8,12 @@ import 'package:flutter/widgets.dart'; /// such as [PageView], [Dismissible], [BottomSheet]. class MagnifierGestureDetectorScope extends InheritedWidget { const MagnifierGestureDetectorScope({ - this.axis, + required this.axis, this.touchSlopFactor = .8, - @required Widget child, + required Widget child, }) : super(child: child); - static MagnifierGestureDetectorScope of(BuildContext context) { + static MagnifierGestureDetectorScope? of(BuildContext context) { final scope = context.dependOnInheritedWidgetOfExactType(); return scope; } diff --git a/lib/widgets/common/magnifier/pan/scroll_physics.dart b/lib/widgets/common/magnifier/pan/scroll_physics.dart index 9f8e14d13..61d3492ec 100644 --- a/lib/widgets/common/magnifier/pan/scroll_physics.dart +++ b/lib/widgets/common/magnifier/pan/scroll_physics.dart @@ -8,7 +8,7 @@ import 'package:flutter/widgets.dart'; class MagnifierScrollerPhysics extends ScrollPhysics { const MagnifierScrollerPhysics({ this.touchSlopFactor = 1, - ScrollPhysics parent, + ScrollPhysics? parent, }) : super(parent: parent); // in [0, 1] @@ -17,7 +17,7 @@ class MagnifierScrollerPhysics extends ScrollPhysics { final double touchSlopFactor; @override - MagnifierScrollerPhysics applyTo(ScrollPhysics ancestor) { + MagnifierScrollerPhysics applyTo(ScrollPhysics? ancestor) { return MagnifierScrollerPhysics( touchSlopFactor: touchSlopFactor, parent: buildParent(ancestor), diff --git a/lib/widgets/common/magnifier/scale/scale_boundaries.dart b/lib/widgets/common/magnifier/scale/scale_boundaries.dart index 30615777a..3c9db4994 100644 --- a/lib/widgets/common/magnifier/scale/scale_boundaries.dart +++ b/lib/widgets/common/magnifier/scale/scale_boundaries.dart @@ -14,11 +14,11 @@ class ScaleBoundaries { final Size childSize; const ScaleBoundaries({ - @required ScaleLevel minScale, - @required ScaleLevel maxScale, - @required ScaleLevel initialScale, - @required this.viewportSize, - @required this.childSize, + required ScaleLevel minScale, + required ScaleLevel maxScale, + required ScaleLevel initialScale, + required this.viewportSize, + required this.childSize, }) : _minScale = minScale, _maxScale = maxScale, _initialScale = initialScale; @@ -51,7 +51,7 @@ class ScaleBoundaries { } Offset viewportToChildPosition(MagnifierController controller, Offset viewportPosition) { - return viewportToStatePosition(controller, viewportPosition) / controller.scale + _childCenter; + return viewportToStatePosition(controller, viewportPosition) / controller.scale! + _childCenter; } Offset childToStatePosition(double scale, Offset childPosition) { diff --git a/lib/widgets/common/magnifier/scale/state.dart b/lib/widgets/common/magnifier/scale/state.dart index 81595109e..1a587bb50 100644 --- a/lib/widgets/common/magnifier/scale/state.dart +++ b/lib/widgets/common/magnifier/scale/state.dart @@ -7,14 +7,14 @@ import 'package:flutter/widgets.dart'; @immutable class ScaleStateChange { const ScaleStateChange({ - @required this.state, - @required this.source, + required this.state, + required this.source, this.childFocalPoint, }); final ScaleState state; final ChangeSource source; - final Offset childFocalPoint; + final Offset? childFocalPoint; @override bool operator ==(Object other) => identical(this, other) || other is ScaleStateChange && runtimeType == other.runtimeType && state == other.state && childFocalPoint == other.childFocalPoint; diff --git a/lib/widgets/common/providers/highlight_info_provider.dart b/lib/widgets/common/providers/highlight_info_provider.dart index 8b09c1695..a1de6717a 100644 --- a/lib/widgets/common/providers/highlight_info_provider.dart +++ b/lib/widgets/common/providers/highlight_info_provider.dart @@ -5,7 +5,7 @@ import 'package:provider/provider.dart'; class HighlightInfoProvider extends StatelessWidget { final Widget child; - const HighlightInfoProvider({@required this.child}); + const HighlightInfoProvider({required this.child}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/common/providers/media_query_data_provider.dart b/lib/widgets/common/providers/media_query_data_provider.dart index 25ba8c4c7..dc44699f9 100644 --- a/lib/widgets/common/providers/media_query_data_provider.dart +++ b/lib/widgets/common/providers/media_query_data_provider.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; class MediaQueryDataProvider extends StatelessWidget { final Widget child; - const MediaQueryDataProvider({@required this.child}); + const MediaQueryDataProvider({required this.child}); @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 0b39401c7..19701e48b 100644 --- a/lib/widgets/common/providers/tile_extent_controller_provider.dart +++ b/lib/widgets/common/providers/tile_extent_controller_provider.dart @@ -7,8 +7,8 @@ class TileExtentControllerProvider extends StatelessWidget { final Widget child; const TileExtentControllerProvider({ - @required this.controller, - @required this.child, + required this.controller, + required this.child, }); @override diff --git a/lib/widgets/common/scaling.dart b/lib/widgets/common/scaling.dart index 702f5dbc8..6cf59b501 100644 --- a/lib/widgets/common/scaling.dart +++ b/lib/widgets/common/scaling.dart @@ -1,8 +1,10 @@ import 'dart:ui' as ui; +import 'package:aves/model/highlight.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; @@ -16,21 +18,17 @@ class ScalerMetadata { class GridScaleGestureDetector extends StatefulWidget { final GlobalKey scrollableKey; - final ValueNotifier appBarHeightNotifier; final Widget Function(Offset center, double extent, Widget child) gridBuilder; final Widget Function(T item, double extent) scaledBuilder; - final Rect Function(BuildContext context, T item) getScaledItemTileRect; - final void Function(T item) onScaled; + final Object Function(T item)? highlightItem; final Widget child; const GridScaleGestureDetector({ - @required this.scrollableKey, - @required this.appBarHeightNotifier, - this.gridBuilder, - @required this.scaledBuilder, - @required this.getScaledItemTileRect, - @required this.onScaled, - @required this.child, + required this.scrollableKey, + required this.gridBuilder, + required this.scaledBuilder, + this.highlightItem, + required this.child, }); @override @@ -38,11 +36,11 @@ class GridScaleGestureDetector extends StatefulWidget { } class _GridScaleGestureDetectorState extends State> { - double _startExtent, _extentMin, _extentMax; + double? _startExtent, _extentMin, _extentMax; bool _applyingScale = false; - ValueNotifier _scaledExtentNotifier; - OverlayEntry _overlayEntry; - ScalerMetadata _metadata; + ValueNotifier? _scaledExtentNotifier; + OverlayEntry? _overlayEntry; + ScalerMetadata? _metadata; @override Widget build(BuildContext context) { @@ -52,19 +50,19 @@ class _GridScaleGestureDetectorState extends State(BoxHitTestResult result) => result.path.firstWhere((el) => el.target is U, orElse: () => null)?.target as U; + U? firstOf(BoxHitTestResult result) => result.path.firstWhereOrNull((el) => el.target is U)?.target as U?; final renderMetaData = firstOf(result); // abort if we cannot find an image to show on overlay if (renderMetaData == null) return; _metadata = renderMetaData.metaData; _startExtent = renderMetaData.size.width; - _scaledExtentNotifier = ValueNotifier(_startExtent); + _scaledExtentNotifier = ValueNotifier(_startExtent!); // not the same as `MediaQuery.size.width`, because of screen insets/padding final gridWidth = scrollableBox.size.width; @@ -73,28 +71,32 @@ class _GridScaleGestureDetectorState extends State ScaleOverlay( - builder: (extent) => widget.scaledBuilder(_metadata.item, extent), + builder: (extent) => SizedBox( + width: extent, + height: extent, + child: widget.scaledBuilder(_metadata!.item, extent), + ), center: thumbnailCenter, viewportWidth: gridWidth, gridBuilder: widget.gridBuilder, - scaledExtentNotifier: _scaledExtentNotifier, + scaledExtentNotifier: _scaledExtentNotifier!, ), ); - Overlay.of(scrollableContext).insert(_overlayEntry); + Overlay.of(scrollableContext)!.insert(_overlayEntry!); }, onScaleUpdate: (details) { if (_scaledExtentNotifier == null) return; final s = details.scale; - _scaledExtentNotifier.value = (_startExtent * s).clamp(_extentMin, _extentMax); + _scaledExtentNotifier!.value = (_startExtent! * s).clamp(_extentMin!, _extentMax!); }, onScaleEnd: (details) { if (_scaledExtentNotifier == null) return; if (_overlayEntry != null) { - _overlayEntry.remove(); + _overlayEntry!.remove(); _overlayEntry = null; } @@ -102,18 +104,16 @@ class _GridScaleGestureDetectorState extends State(); final oldExtent = tileExtentController.extentNotifier.value; // sanitize and update grid layout if necessary - final newExtent = tileExtentController.setUserPreferredExtent(_scaledExtentNotifier.value); + final newExtent = tileExtentController.setUserPreferredExtent(_scaledExtentNotifier!.value); _scaledExtentNotifier = null; if (newExtent == oldExtent) { _applyingScale = false; } else { // scroll to show the focal point thumbnail at its new position - WidgetsBinding.instance.addPostFrameCallback((_) { - final entry = _metadata.item; - _scrollToItem(entry); - // warning: posting `onScaled` in the next frame with `addPostFrameCallback` - // would trigger only when the scrollable offset actually changes - Future.delayed(Durations.collectionScalingCompleteNotificationDelay).then((_) => widget.onScaled?.call(entry)); + WidgetsBinding.instance!.addPostFrameCallback((_) { + final trackItem = _metadata!.item; + final highlightItem = widget.highlightItem?.call(trackItem) ?? trackItem; + context.read().trackItem(trackItem, animate: false, highlightItem: highlightItem); _applyingScale = false; }); } @@ -130,26 +130,6 @@ class _GridScaleGestureDetectorState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => _init = true)); + WidgetsBinding.instance!.addPostFrameCallback((_) => setState(() => _init = true)); } @override @@ -201,7 +181,7 @@ class _ScaleOverlayState extends State { ], ), ) - : BoxDecoration( + : const BoxDecoration( // provide dummy gradient to lerp to the other one during animation gradient: RadialGradient( colors: [ @@ -232,13 +212,13 @@ class _ScaleOverlayState extends State { left: clampedCenter.dx - extent / 2, top: clampedCenter.dy - extent / 2, child: DefaultTextStyle( - style: TextStyle(), + style: const TextStyle(), child: child, ), ), ], ); - child = widget.gridBuilder?.call(clampedCenter, extent, child) ?? child; + child = widget.gridBuilder(clampedCenter, extent, child); return child; }, ), @@ -251,47 +231,60 @@ class _ScaleOverlayState extends State { class GridPainter extends CustomPainter { final Offset center; - final double extent, spacing; - final double strokeWidth; + final double extent, spacing, borderWidth; + final Radius borderRadius; final Color color; const GridPainter({ - @required this.center, - @required this.extent, - this.spacing = 0.0, - this.strokeWidth = 1.0, - @required this.color, + required this.center, + required this.extent, + required this.spacing, + required this.borderWidth, + required this.borderRadius, + required this.color, }); @override void paint(Canvas canvas, Size size) { - final radius = extent * 3; - final paint = Paint() - ..strokeWidth = strokeWidth + final strokePaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = borderWidth ..shader = ui.Gradient.radial( center, - radius, + extent * 2, [ color, Colors.transparent, ], [ - extent / radius, + .8, 1, ], ); - void draw(Offset topLeft) { - for (var i = -1; i <= 2; i++) { - final ref = (extent + spacing) * i; - canvas.drawLine(Offset(0, topLeft.dy + ref), Offset(size.width, topLeft.dy + ref), paint); - canvas.drawLine(Offset(topLeft.dx + ref, 0), Offset(topLeft.dx + ref, size.height), paint); - } - } + final fillPaint = Paint() + ..style = PaintingStyle.fill + ..color = color.withOpacity(.25); - final topLeft = center.translate(-extent / 2, -extent / 2); - draw(topLeft); - if (spacing > 0) { - draw(topLeft.translate(-spacing, -spacing)); + final delta = extent + spacing; + for (var i = -2; i <= 2; i++) { + final dx = delta * i; + for (var j = -2; j <= 2; j++) { + if (i == 0 && j == 0) continue; + final dy = delta * j; + final rect = RRect.fromRectAndRadius( + Rect.fromCenter( + center: center + Offset(dx, dy), + width: extent, + height: extent, + ), + borderRadius, + ); + + if ((i.abs() == 1 && j == 0) || (j.abs() == 1 && i == 0)) { + canvas.drawRRect(rect, fillPaint); + } + canvas.drawRRect(rect, strokePaint); + } } } diff --git a/lib/widgets/common/tile_extent_controller.dart b/lib/widgets/common/tile_extent_controller.dart index 9500d1641..5ee44678b 100644 --- a/lib/widgets/common/tile_extent_controller.dart +++ b/lib/widgets/common/tile_extent_controller.dart @@ -10,17 +10,17 @@ class TileExtentController { final double spacing, extentMin, extentMax; final ValueNotifier extentNotifier = ValueNotifier(0); - Size _viewportSize; + Size _viewportSize = Size.zero; Size get viewportSize => _viewportSize; TileExtentController({ - @required this.settingsRouteKey, + required this.settingsRouteKey, this.columnCountMin = 2, - @required this.columnCountDefault, - @required this.extentMin, + required this.columnCountDefault, + required this.extentMin, this.extentMax = 300, - @required this.spacing, + required this.spacing, }); void setViewportSize(Size viewportSize) { diff --git a/lib/widgets/debug/android_apps.dart b/lib/widgets/debug/android_apps.dart index 9854088c1..fa39ddb39 100644 --- a/lib/widgets/debug/android_apps.dart +++ b/lib/widgets/debug/android_apps.dart @@ -12,7 +12,7 @@ class DebugAndroidAppSection extends StatefulWidget { } class _DebugAndroidAppSectionState extends State with AutomaticKeepAliveClientMixin { - Future> _loader; + late Future> _loader; static const iconSize = 20.0; @@ -30,15 +30,15 @@ class _DebugAndroidAppSectionState extends State with Au title: 'Android Apps', children: [ Padding( - padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), child: FutureBuilder>( future: _loader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); - final packages = snapshot.data.toList()..sort((a, b) => compareAsciiUpperCase(a.packageName, b.packageName)); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + final packages = snapshot.data!.toList()..sort((a, b) => compareAsciiUpperCase(a.packageName, b.packageName)); final enabledTheme = IconTheme.of(context); - final disabledTheme = enabledTheme.merge(IconThemeData(opacity: .2)); + final disabledTheme = enabledTheme.merge(const IconThemeData(opacity: .2)); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: packages.map((package) { @@ -64,7 +64,7 @@ class _DebugAndroidAppSectionState extends State with Au alignment: PlaceholderAlignment.middle, child: IconTheme( data: package.categoryLauncher ? enabledTheme : disabledTheme, - child: Icon( + child: const Icon( Icons.launch_outlined, size: iconSize, ), @@ -74,7 +74,7 @@ class _DebugAndroidAppSectionState extends State with Au alignment: PlaceholderAlignment.middle, child: IconTheme( data: package.isSystem ? enabledTheme : disabledTheme, - child: Icon( + child: const Icon( Icons.android, size: iconSize, ), diff --git a/lib/widgets/debug/android_dirs.dart b/lib/widgets/debug/android_dirs.dart index 92e71bdf7..98e49f0cc 100644 --- a/lib/widgets/debug/android_dirs.dart +++ b/lib/widgets/debug/android_dirs.dart @@ -11,7 +11,7 @@ class DebugAndroidDirSection extends StatefulWidget { } class _DebugAndroidDirSectionState extends State with AutomaticKeepAliveClientMixin { - Future _loader; + late Future _loader; @override void initState() { @@ -27,13 +27,13 @@ class _DebugAndroidDirSectionState extends State with Au title: 'Android Dirs', children: [ Padding( - padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), child: FutureBuilder( future: _loader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); - final data = SplayTreeMap.of(snapshot.data.map((k, v) => MapEntry(k.toString(), v?.toString() ?? 'null'))); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + final data = SplayTreeMap.of(snapshot.data!.map((k, v) => MapEntry(k.toString(), v?.toString() ?? 'null'))); return InfoRowGroup(data); }, ), diff --git a/lib/widgets/debug/android_env.dart b/lib/widgets/debug/android_env.dart index 0600393dd..a7893c609 100644 --- a/lib/widgets/debug/android_env.dart +++ b/lib/widgets/debug/android_env.dart @@ -11,7 +11,7 @@ class DebugAndroidEnvironmentSection extends StatefulWidget { } class _DebugAndroidEnvironmentSectionState extends State with AutomaticKeepAliveClientMixin { - Future _loader; + late Future _loader; @override void initState() { @@ -27,13 +27,13 @@ class _DebugAndroidEnvironmentSectionState extends State( future: _loader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); - final data = SplayTreeMap.of(snapshot.data.map((k, v) => MapEntry(k.toString(), v?.toString() ?? 'null'))); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + final data = SplayTreeMap.of(snapshot.data!.map((k, v) => MapEntry(k.toString(), v?.toString() ?? 'null'))); return InfoRowGroup(data); }, ), diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index d4698e79e..ce6a93b87 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -28,18 +28,18 @@ class _AppDebugPageState extends State { Set get visibleEntries => source.visibleEntries; - static OverlayEntry _taskQueueOverlayEntry; + static OverlayEntry? _taskQueueOverlayEntry; @override Widget build(BuildContext context) { return MediaQueryDataProvider( child: Scaffold( appBar: AppBar( - title: Text('Debug'), + title: const Text('Debug'), ), body: SafeArea( child: ListView( - padding: EdgeInsets.all(8), + padding: const EdgeInsets.all(8), children: [ _buildGeneralTabView(), DebugAndroidAppSection(), @@ -65,7 +65,7 @@ class _AppDebugPageState extends State { return AvesExpansionTile( title: 'General', children: [ - Padding( + const Padding( padding: EdgeInsets.all(8), child: Text('Time dilation'), ), @@ -85,17 +85,17 @@ class _AppDebugPageState extends State { _taskQueueOverlayEntry = OverlayEntry( builder: (context) => DebugTaskQueueOverlay(), ); - Overlay.of(context).insert(_taskQueueOverlayEntry); + Overlay.of(context)!.insert(_taskQueueOverlayEntry!); } else { _taskQueueOverlayEntry = null; } setState(() {}); }, - title: Text('Show tasks overlay'), + title: const Text('Show tasks overlay'), ), - Divider(), + const Divider(), Padding( - padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), child: InfoRowGroup( { 'All entries': '${source.allEntries.length}', diff --git a/lib/widgets/debug/cache.dart b/lib/widgets/debug/cache.dart index cc9842fe4..8ab1cf38b 100644 --- a/lib/widgets/debug/cache.dart +++ b/lib/widgets/debug/cache.dart @@ -18,22 +18,22 @@ class _DebugCacheSectionState extends State with AutomaticKee title: 'Cache', children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric(horizontal: 8), child: Column( children: [ Row( children: [ Expanded( - child: Text('Image cache:\n\t${imageCache.currentSize}/${imageCache.maximumSize} items\n\t${formatFilesize(imageCache.currentSizeBytes)}/${formatFilesize(imageCache.maximumSizeBytes)}'), + child: Text('Image cache:\n\t${imageCache!.currentSize}/${imageCache!.maximumSize} items\n\t${formatFilesize(imageCache!.currentSizeBytes)}/${formatFilesize(imageCache!.maximumSizeBytes)}'), ), - SizedBox(width: 8), + const SizedBox(width: 8), ElevatedButton( onPressed: () { - imageCache.clear(); + imageCache!.clear(); setState(() {}); }, - child: Text('Clear'), + child: const Text('Clear'), ), ], ), @@ -42,26 +42,26 @@ class _DebugCacheSectionState extends State with AutomaticKee Expanded( child: Text('SVG cache: ${PictureProvider.cache.count} items'), ), - SizedBox(width: 8), + const SizedBox(width: 8), ElevatedButton( onPressed: () { PictureProvider.cache.clear(); setState(() {}); }, - child: Text('Clear'), + child: const Text('Clear'), ), ], ), Row( children: [ - Expanded( + const Expanded( child: Text('Glide disk cache: ?'), ), - SizedBox(width: 8), + const SizedBox(width: 8), ElevatedButton( onPressed: imageFileService.clearSizedThumbnailDiskCache, - child: Text('Clear'), + child: const Text('Clear'), ), ], ), diff --git a/lib/widgets/debug/database.dart b/lib/widgets/debug/database.dart index 9dd19894c..ef7e25d89 100644 --- a/lib/widgets/debug/database.dart +++ b/lib/widgets/debug/database.dart @@ -13,13 +13,13 @@ class DebugAppDatabaseSection extends StatefulWidget { } class _DebugAppDatabaseSectionState extends State with AutomaticKeepAliveClientMixin { - Future _dbFileSizeLoader; - Future> _dbEntryLoader; - Future> _dbDateLoader; - Future> _dbMetadataLoader; - Future> _dbAddressLoader; - Future> _dbFavouritesLoader; - Future> _dbCoversLoader; + late Future _dbFileSizeLoader; + late Future> _dbEntryLoader; + late Future> _dbDateLoader; + late Future> _dbMetadataLoader; + late Future> _dbAddressLoader; + late Future> _dbFavouritesLoader; + late Future> _dbCoversLoader; @override void initState() { @@ -35,7 +35,7 @@ class _DebugAppDatabaseSectionState extends State with title: 'Database', children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric(horizontal: 8), child: Column( children: [ FutureBuilder( @@ -43,17 +43,17 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); return Row( children: [ Expanded( - child: Text('DB file size: ${formatFilesize(snapshot.data)}'), + child: Text('DB file size: ${formatFilesize(snapshot.data!)}'), ), - SizedBox(width: 8), + const SizedBox(width: 8), ElevatedButton( onPressed: () => metadataDb.reset().then((_) => _startDbReport()), - child: Text('Reset'), + child: const Text('Reset'), ), ], ); @@ -64,17 +64,17 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); return Row( children: [ Expanded( - child: Text('entry rows: ${snapshot.data.length}'), + child: Text('entry rows: ${snapshot.data!.length}'), ), - SizedBox(width: 8), + const SizedBox(width: 8), ElevatedButton( onPressed: () => metadataDb.clearEntries().then((_) => _startDbReport()), - child: Text('Clear'), + child: const Text('Clear'), ), ], ); @@ -85,17 +85,17 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); return Row( children: [ Expanded( - child: Text('date rows: ${snapshot.data.length}'), + child: Text('date rows: ${snapshot.data!.length}'), ), - SizedBox(width: 8), + const SizedBox(width: 8), ElevatedButton( onPressed: () => metadataDb.clearDates().then((_) => _startDbReport()), - child: Text('Clear'), + child: const Text('Clear'), ), ], ); @@ -106,17 +106,17 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); return Row( children: [ Expanded( - child: Text('metadata rows: ${snapshot.data.length}'), + child: Text('metadata rows: ${snapshot.data!.length}'), ), - SizedBox(width: 8), + const SizedBox(width: 8), ElevatedButton( onPressed: () => metadataDb.clearMetadataEntries().then((_) => _startDbReport()), - child: Text('Clear'), + child: const Text('Clear'), ), ], ); @@ -127,17 +127,17 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); return Row( children: [ Expanded( - child: Text('address rows: ${snapshot.data.length}'), + child: Text('address rows: ${snapshot.data!.length}'), ), - SizedBox(width: 8), + const SizedBox(width: 8), ElevatedButton( onPressed: () => metadataDb.clearAddresses().then((_) => _startDbReport()), - child: Text('Clear'), + child: const Text('Clear'), ), ], ); @@ -148,17 +148,17 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); return Row( children: [ Expanded( - child: Text('favourite rows: ${snapshot.data.length} (${favourites.count} in memory)'), + child: Text('favourite rows: ${snapshot.data!.length} (${favourites.count} in memory)'), ), - SizedBox(width: 8), + const SizedBox(width: 8), ElevatedButton( onPressed: () => favourites.clear().then((_) => _startDbReport()), - child: Text('Clear'), + child: const Text('Clear'), ), ], ); @@ -169,17 +169,17 @@ class _DebugAppDatabaseSectionState extends State with builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); return Row( children: [ Expanded( - child: Text('cover rows: ${snapshot.data.length} (${covers.count} in memory)'), + child: Text('cover rows: ${snapshot.data!.length} (${covers.count} in memory)'), ), - SizedBox(width: 8), + const SizedBox(width: 8), ElevatedButton( onPressed: () => covers.clear().then((_) => _startDbReport()), - child: Text('Clear'), + child: const Text('Clear'), ), ], ); diff --git a/lib/widgets/debug/firebase.dart b/lib/widgets/debug/firebase.dart index c1069f07e..c63291a74 100644 --- a/lib/widgets/debug/firebase.dart +++ b/lib/widgets/debug/firebase.dart @@ -12,26 +12,26 @@ class DebugFirebaseSection extends StatelessWidget { title: 'Firebase', children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( children: [ ElevatedButton( onPressed: FirebaseCrashlytics.instance.crash, - child: Text('Crash'), + child: const Text('Crash'), ), - SizedBox(width: 8), + const SizedBox(width: 8), ElevatedButton( onPressed: () => FirebaseAnalytics().logEvent( name: 'debug_test', parameters: {'time': DateTime.now().toIso8601String()}, ), - child: Text('Send event'), + child: const Text('Send event'), ), ], ), ), Padding( - padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), child: InfoRowGroup({ 'Firebase data collection enabled': '${Firebase.app().isAutomaticDataCollectionEnabled}', 'Crashlytics collection enabled': '${FirebaseCrashlytics.instance.isCrashlyticsCollectionEnabled}', diff --git a/lib/widgets/debug/overlay.dart b/lib/widgets/debug/overlay.dart index 9fb6b0f06..8fcae7b2c 100644 --- a/lib/widgets/debug/overlay.dart +++ b/lib/widgets/debug/overlay.dart @@ -6,20 +6,20 @@ class DebugTaskQueueOverlay extends StatelessWidget { Widget build(BuildContext context) { return IgnorePointer( child: DefaultTextStyle( - style: TextStyle(), + style: const TextStyle(), child: Align( alignment: AlignmentDirectional.bottomStart, child: SafeArea( child: Container( - color: Colors.indigo[900].withAlpha(0xCC), - padding: EdgeInsets.all(8), + color: Colors.indigo.shade900.withAlpha(0xCC), + padding: const EdgeInsets.all(8), child: StreamBuilder( stream: servicePolicy.queueStream, builder: (context, snapshot) { - if (snapshot.hasError) return SizedBox.shrink(); + if (snapshot.hasError) return const SizedBox.shrink(); final queuedEntries = >[]; if (snapshot.hasData) { - final state = snapshot.data; + final state = snapshot.data!; queuedEntries.add(MapEntry('run', state.runningCount)); queuedEntries.add(MapEntry('paused', state.pausedCount)); queuedEntries.addAll(state.queueByPriority.entries.map((kv) => MapEntry(kv.key.toString(), kv.value))); diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart index e83bba3a5..3878a47bd 100644 --- a/lib/widgets/debug/settings.dart +++ b/lib/widgets/debug/settings.dart @@ -18,19 +18,19 @@ class DebugSettingsSection extends StatelessWidget { title: 'Settings', children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric(horizontal: 8), child: ElevatedButton( onPressed: () => settings.reset(), - child: Text('Reset'), + child: const Text('Reset'), ), ), SwitchListTile( value: settings.hasAcceptedTerms, onChanged: (v) => settings.hasAcceptedTerms = v, - title: Text('hasAcceptedTerms'), + title: const Text('hasAcceptedTerms'), ), Padding( - padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), child: InfoRowGroup({ 'tileExtent - Collection': '${settings.getTileExtent(CollectionPage.routeName)}', 'tileExtent - Albums': '${settings.getTileExtent(AlbumListPage.routeName)}', @@ -43,7 +43,7 @@ class DebugSettingsSection extends StatelessWidget { 'searchHistory': toMultiline(settings.searchHistory), 'lastVersionCheckDate': '${settings.lastVersionCheckDate}', 'locale': '${settings.locale}', - 'systemLocale': '${WidgetsBinding.instance.window.locale}', + 'systemLocale': '${WidgetsBinding.instance!.window.locale}', }), ), ], diff --git a/lib/widgets/debug/storage.dart b/lib/widgets/debug/storage.dart index 9272ecfaa..d4b9ffb12 100644 --- a/lib/widgets/debug/storage.dart +++ b/lib/widgets/debug/storage.dart @@ -11,7 +11,7 @@ class DebugStorageSection extends StatefulWidget { } class _DebugStorageSectionState extends State with AutomaticKeepAliveClientMixin { - final Map _freeSpaceByVolume = {}; + final Map _freeSpaceByVolume = {}; @override void initState() { @@ -33,11 +33,11 @@ class _DebugStorageSectionState extends State with Automati final freeSpace = _freeSpaceByVolume[v.path]; return [ Padding( - padding: EdgeInsets.all(8), + padding: const EdgeInsets.all(8), child: Text(v.path), ), Padding( - padding: EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric(horizontal: 8), child: InfoRowGroup({ 'description': '${v.getDescription(context)}', 'isPrimary': '${v.isPrimary}', @@ -46,7 +46,7 @@ class _DebugStorageSectionState extends State with Automati if (freeSpace != null) 'freeSpace': formatFilesize(freeSpace), }), ), - Divider(), + const Divider(), ]; }) ], diff --git a/lib/widgets/dialogs/add_shortcut_dialog.dart b/lib/widgets/dialogs/add_shortcut_dialog.dart index 61b7a7e2f..82b9c1900 100644 --- a/lib/widgets/dialogs/add_shortcut_dialog.dart +++ b/lib/widgets/dialogs/add_shortcut_dialog.dart @@ -7,6 +7,7 @@ import 'package:aves/widgets/collection/thumbnail/vector.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/dialogs/item_pick_dialog.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -18,8 +19,8 @@ class AddShortcutDialog extends StatefulWidget { final String defaultName; const AddShortcutDialog({ - @required this.collection, - @required this.defaultName, + required this.collection, + required this.defaultName, }); @override @@ -29,7 +30,7 @@ class AddShortcutDialog extends StatefulWidget { class _AddShortcutDialogState extends State { final TextEditingController _nameController = TextEditingController(); final ValueNotifier _isValidNotifier = ValueNotifier(false); - AvesEntry _coverEntry; + AvesEntry? _coverEntry; CollectionLens get collection => widget.collection; @@ -40,7 +41,7 @@ class _AddShortcutDialogState extends State { super.initState(); final entries = collection.sortedEntries; if (entries.isNotEmpty) { - final coverEntries = filters.map(covers.coverContentId).where((id) => id != null).map((id) => entries.firstWhere((entry) => entry.contentId == id, orElse: () => null)).where((entry) => entry != null); + final coverEntries = filters.map(covers.coverContentId).where((id) => id != null).map((id) => entries.firstWhereOrNull((entry) => entry.contentId == id)).where((entry) => entry != null); _coverEntry = coverEntries.isNotEmpty ? coverEntries.first : entries.first; } _nameController.text = widget.defaultName; @@ -66,11 +67,11 @@ class _AddShortcutDialogState extends State { if (_coverEntry != null) Container( alignment: Alignment.center, - padding: EdgeInsets.only(top: 16), - child: _buildCover(_coverEntry, extent), + padding: const EdgeInsets.only(top: 16), + child: _buildCover(_coverEntry!, extent), ), Padding( - padding: EdgeInsets.symmetric(vertical: 8, horizontal: 24), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24), child: TextField( controller: _nameController, decoration: InputDecoration( @@ -108,7 +109,7 @@ class _AddShortcutDialogState extends State { return GestureDetector( onTap: _pickEntry, child: ClipRRect( - borderRadius: BorderRadius.circular(32), + borderRadius: const BorderRadius.all(Radius.circular(32)), child: SizedBox( width: extent, height: extent, @@ -130,7 +131,7 @@ class _AddShortcutDialogState extends State { final entry = await Navigator.push( context, MaterialPageRoute( - settings: RouteSettings(name: ItemPickDialog.routeName), + settings: const RouteSettings(name: ItemPickDialog.routeName), builder: (context) => ItemPickDialog( CollectionLens( source: collection.source, @@ -147,9 +148,9 @@ class _AddShortcutDialogState extends State { } Future _validate() async { - final name = _nameController.text ?? ''; + final name = _nameController.text; _isValidNotifier.value = name.isNotEmpty; } - void _submit(BuildContext context) => Navigator.pop(context, Tuple2(_coverEntry, _nameController.text)); + void _submit(BuildContext context) => Navigator.pop(context, Tuple2(_coverEntry, _nameController.text)); } diff --git a/lib/widgets/dialogs/aves_dialog.dart b/lib/widgets/dialogs/aves_dialog.dart index 1542726bd..c47c4258a 100644 --- a/lib/widgets/dialogs/aves_dialog.dart +++ b/lib/widgets/dialogs/aves_dialog.dart @@ -9,18 +9,18 @@ class AvesDialog extends AlertDialog { static const borderWidth = 1.0; AvesDialog({ - @required BuildContext context, - String title, - ScrollController scrollController, - List scrollableContent, - Widget content, - @required List actions, + required BuildContext context, + String? title, + ScrollController? scrollController, + List? scrollableContent, + Widget? content, + required List actions, }) : assert((scrollableContent != null) ^ (content != null)), super( title: title != null ? Padding( // padding to avoid transparent border overlapping - padding: EdgeInsets.symmetric(horizontal: borderWidth), + padding: const EdgeInsets.symmetric(horizontal: borderWidth), child: DialogTitle(title: title), ) : null, @@ -32,7 +32,7 @@ class AvesDialog extends AlertDialog { content: scrollableContent != null ? Container( // padding to avoid transparent border overlapping - padding: EdgeInsets.symmetric(horizontal: borderWidth), + padding: const EdgeInsets.symmetric(horizontal: borderWidth), // workaround because the dialog tries // to size itself to the content intrinsic size, // but the `ListView` viewport does not have one @@ -51,12 +51,12 @@ class AvesDialog extends AlertDialog { ), ) : content, - contentPadding: scrollableContent != null ? EdgeInsets.zero : EdgeInsets.fromLTRB(24, 20, 24, 0), + contentPadding: scrollableContent != null ? EdgeInsets.zero : const EdgeInsets.fromLTRB(24, 20, 24, 0), actions: actions, - actionsPadding: EdgeInsets.symmetric(horizontal: 8), + actionsPadding: const EdgeInsets.symmetric(horizontal: 8), shape: RoundedRectangleBorder( side: Divider.createBorderSide(context, width: borderWidth), - borderRadius: BorderRadius.circular(24), + borderRadius: const BorderRadius.all(Radius.circular(24)), ), ); } @@ -64,7 +64,7 @@ class AvesDialog extends AlertDialog { class DialogTitle extends StatelessWidget { final String title; - const DialogTitle({@required this.title}); + const DialogTitle({required this.title}); @override Widget build(BuildContext context) { @@ -78,7 +78,7 @@ class DialogTitle extends StatelessWidget { ), child: Text( title, - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.normal, fontFeatures: [FontFeature.enable('smcp')], ), diff --git a/lib/widgets/dialogs/aves_selection_dialog.dart b/lib/widgets/dialogs/aves_selection_dialog.dart index 99af9d174..c1323b2e0 100644 --- a/lib/widgets/dialogs/aves_selection_dialog.dart +++ b/lib/widgets/dialogs/aves_selection_dialog.dart @@ -9,14 +9,14 @@ typedef TextBuilder = String Function(T value); class AvesSelectionDialog extends StatefulWidget { final T initialValue; final Map options; - final TextBuilder optionSubtitleBuilder; + final TextBuilder? optionSubtitleBuilder; final String title; const AvesSelectionDialog({ - @required this.initialValue, - @required this.options, + required this.initialValue, + required this.options, this.optionSubtitleBuilder, - @required this.title, + required this.title, }); @override @@ -24,7 +24,7 @@ class AvesSelectionDialog extends StatefulWidget { } class _AvesSelectionDialogState extends State> { - T _selectedValue; + late T _selectedValue; @override void initState() { diff --git a/lib/widgets/dialogs/cover_selection_dialog.dart b/lib/widgets/dialogs/cover_selection_dialog.dart index e939810f9..eefa7f90d 100644 --- a/lib/widgets/dialogs/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/cover_selection_dialog.dart @@ -14,11 +14,11 @@ import 'package:tuple/tuple.dart'; class CoverSelectionDialog extends StatefulWidget { final CollectionFilter filter; - final AvesEntry customEntry; + final AvesEntry? customEntry; const CoverSelectionDialog({ - @required this.filter, - @required this.customEntry, + required this.filter, + required this.customEntry, }); @override @@ -26,8 +26,8 @@ class CoverSelectionDialog extends StatefulWidget { } class _CoverSelectionDialogState extends State { - bool _isCustom; - AvesEntry _customEntry, _recentEntry; + late bool _isCustom; + AvesEntry? _customEntry, _recentEntry; CollectionFilter get filter => widget.filter; @@ -59,10 +59,11 @@ class _CoverSelectionDialogState extends State { overflow: TextOverflow.fade, maxLines: 1, ); - return RadioListTile( + return RadioListTile( value: isCustom, groupValue: _isCustom, onChanged: (v) { + if (v == null) return; if (v && _customEntry == null) { _pickEntry(); return; @@ -73,11 +74,11 @@ class _CoverSelectionDialogState extends State { title: isCustom ? Row(children: [ title, - Spacer(), + const Spacer(), IconButton( onPressed: _isCustom ? _pickEntry : null, tooltip: 'Change', - icon: Icon(AIcons.setCover), + icon: const Icon(AIcons.setCover), ), ]) : title, @@ -86,7 +87,7 @@ class _CoverSelectionDialogState extends State { ), Container( alignment: Alignment.center, - padding: EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.only(bottom: 16), child: DecoratedFilterChip( filter: filter, extent: extent, @@ -101,7 +102,7 @@ class _CoverSelectionDialogState extends State { child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), TextButton( - onPressed: () => Navigator.pop(context, Tuple2(_isCustom, _customEntry)), + onPressed: () => Navigator.pop(context, Tuple2(_isCustom, _customEntry)), child: Text(l10n.applyButtonLabel), ), ], @@ -115,7 +116,7 @@ class _CoverSelectionDialogState extends State { final entry = await Navigator.push( context, MaterialPageRoute( - settings: RouteSettings(name: ItemPickDialog.routeName), + settings: const RouteSettings(name: ItemPickDialog.routeName), builder: (context) => ItemPickDialog( CollectionLens( source: context.read(), diff --git a/lib/widgets/dialogs/create_album_dialog.dart b/lib/widgets/dialogs/create_album_dialog.dart index 8b1852247..c66a98d53 100644 --- a/lib/widgets/dialogs/create_album_dialog.dart +++ b/lib/widgets/dialogs/create_album_dialog.dart @@ -21,8 +21,8 @@ class _CreateAlbumDialogState extends State { final FocusNode _nameFieldFocusNode = FocusNode(); final ValueNotifier _existsNotifier = ValueNotifier(false); final ValueNotifier _isValidNotifier = ValueNotifier(false); - Set _allVolumes; - StorageVolume _primaryVolume, _selectedVolume; + late Set _allVolumes; + late StorageVolume _primaryVolume, _selectedVolume; @override void initState() { @@ -46,16 +46,16 @@ class _CreateAlbumDialogState extends State { if (_allVolumes.length > 1) { final byPrimary = groupBy(_allVolumes, (volume) => volume.isPrimary); int compare(StorageVolume a, StorageVolume b) => compareAsciiUpperCase(a.path, b.path); - final primaryVolumes = byPrimary[true]..sort(compare); - final otherVolumes = byPrimary[false]..sort(compare); + final primaryVolumes = (byPrimary[true] ?? [])..sort(compare); + final otherVolumes = (byPrimary[false] ?? [])..sort(compare); volumeTiles.addAll([ Padding( - padding: AvesDialog.contentHorizontalPadding + EdgeInsets.only(top: 20), + padding: AvesDialog.contentHorizontalPadding + const EdgeInsets.only(top: 20), child: Text(context.l10n.newAlbumDialogStorageLabel), ), ...primaryVolumes.map((volume) => _buildVolumeTile(context, volume)), ...otherVolumes.map((volume) => _buildVolumeTile(context, volume)), - SizedBox(height: 8), + const SizedBox(height: 8), ]); } @@ -66,7 +66,7 @@ class _CreateAlbumDialogState extends State { scrollableContent: [ ...volumeTiles, Padding( - padding: AvesDialog.contentHorizontalPadding + EdgeInsets.only(bottom: 8), + padding: AvesDialog.contentHorizontalPadding + const EdgeInsets.only(bottom: 8), child: ValueListenableBuilder( valueListenable: _existsNotifier, builder: (context, exists, child) { @@ -106,7 +106,7 @@ class _CreateAlbumDialogState extends State { value: volume, groupValue: _selectedVolume, onChanged: (volume) { - _selectedVolume = volume; + _selectedVolume = volume!; _validate(); setState(() {}); }, @@ -142,12 +142,12 @@ class _CreateAlbumDialogState extends State { } String _buildAlbumPath(String name) { - if (name == null || name.isEmpty) return ''; + if (name.isEmpty) return ''; return pContext.join(_selectedVolume.path, 'Pictures', name); } Future _validate() async { - final newName = _nameController.text ?? ''; + final newName = _nameController.text; final path = _buildAlbumPath(newName); final exists = newName.isNotEmpty && await Directory(path).exists(); _existsNotifier.value = exists; diff --git a/lib/widgets/dialogs/item_pick_dialog.dart b/lib/widgets/dialogs/item_pick_dialog.dart index 013f14fe4..03cf7c8f0 100644 --- a/lib/widgets/dialogs/item_pick_dialog.dart +++ b/lib/widgets/dialogs/item_pick_dialog.dart @@ -38,7 +38,7 @@ class _ItemPickDialogState extends State { bottom: false, child: ChangeNotifierProvider.value( value: collection, - child: CollectionGrid( + child: const CollectionGrid( settingsRouteKey: CollectionPage.routeName, ), ), diff --git a/lib/widgets/dialogs/rename_album_dialog.dart b/lib/widgets/dialogs/rename_album_dialog.dart index 07ee1a9e2..2c9a09726 100644 --- a/lib/widgets/dialogs/rename_album_dialog.dart +++ b/lib/widgets/dialogs/rename_album_dialog.dart @@ -74,12 +74,12 @@ class _RenameAlbumDialogState extends State { } String _buildAlbumPath(String name) { - if (name == null || name.isEmpty) return ''; + if (name.isEmpty) return ''; return pContext.join(pContext.dirname(album), name); } Future _validate() async { - final newName = _nameController.text ?? ''; + final newName = _nameController.text; final path = _buildAlbumPath(newName); final exists = newName.isNotEmpty && await FileSystemEntity.type(path) != FileSystemEntityType.notFound; _existsNotifier.value = exists && newName != initialValue; diff --git a/lib/widgets/dialogs/rename_entry_dialog.dart b/lib/widgets/dialogs/rename_entry_dialog.dart index a7c25692c..39c343e84 100644 --- a/lib/widgets/dialogs/rename_entry_dialog.dart +++ b/lib/widgets/dialogs/rename_entry_dialog.dart @@ -25,7 +25,7 @@ class _RenameEntryDialogState extends State { @override void initState() { super.initState(); - _nameController.text = entry.filenameWithoutExtension ?? entry.sourceTitle; + _nameController.text = entry.filenameWithoutExtension ?? entry.sourceTitle ?? ''; _validate(); } @@ -68,12 +68,12 @@ class _RenameEntryDialogState extends State { } String _buildEntryPath(String name) { - if (name == null || name.isEmpty) return ''; - return pContext.join(entry.directory, name + entry.extension); + if (name.isEmpty) return ''; + return pContext.join(entry.directory!, name + entry.extension!); } Future _validate() async { - final newName = _nameController.text ?? ''; + final newName = _nameController.text; final path = _buildEntryPath(newName); final exists = newName.isNotEmpty && await FileSystemEntity.type(path) != FileSystemEntityType.notFound; _isValidNotifier.value = newName.isNotEmpty && !exists; diff --git a/lib/widgets/drawer/album_tile.dart b/lib/widgets/drawer/album_tile.dart index 0c2706846..d9e1f5830 100644 --- a/lib/widgets/drawer/album_tile.dart +++ b/lib/widgets/drawer/album_tile.dart @@ -19,11 +19,11 @@ class AlbumTile extends StatelessWidget { return CollectionNavTile( leading: IconUtils.getAlbumIcon( context: context, - album: album, + albumPath: album, ), title: displayName, trailing: androidFileUtils.isOnRemovableStorage(album) - ? Icon( + ? const Icon( AIcons.removableStorage, size: 16, color: Colors.grey, diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index e11c4b497..b7c6b80dc 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -33,7 +33,7 @@ class AppDrawer extends StatefulWidget { } class _AppDrawerState extends State { - Future _newVersionLoader; + late Future _newVersionLoader; CollectionSource get source => context.read(); @@ -54,12 +54,12 @@ class _AppDrawerState extends State { if (showVideos) videoTile, if (showFavourites) favouriteTile, _buildSpecialAlbumSection(), - Divider(), + const Divider(), albumListTile, countryListTile, tagListTile, - if (kDebugMode) ...[ - Divider(), + if (!kReleaseMode) ...[ + const Divider(), debugTile, ], ]; @@ -75,7 +75,7 @@ class _AppDrawerState extends State { padding: EdgeInsets.only(bottom: mqPaddingBottom), child: IconTheme( data: iconTheme.copyWith( - size: iconTheme.size * MediaQuery.textScaleFactorOf(context), + size: iconTheme.size! * MediaQuery.textScaleFactorOf(context), ), child: Column( children: drawerItems, @@ -101,7 +101,7 @@ class _AppDrawerState extends State { } return Container( - padding: EdgeInsets.only(left: 16, top: 16, right: 16, bottom: 8), + padding: const EdgeInsets.only(left: 16, top: 16, right: 16, bottom: 8), color: Theme.of(context).accentColor, child: SafeArea( bottom: false, @@ -114,10 +114,10 @@ class _AppDrawerState extends State { spacing: 16, crossAxisAlignment: WrapCrossAlignment.center, children: [ - AvesLogo(size: 64), + const AvesLogo(size: 64), Text( context.l10n.appName, - style: TextStyle( + style: const TextStyle( fontSize: 44, fontWeight: FontWeight.w300, letterSpacing: 1.0, @@ -127,7 +127,7 @@ class _AppDrawerState extends State { ], ), ), - SizedBox(height: 8), + const SizedBox(height: 8), OutlinedButtonTheme( data: OutlinedButtonThemeData( style: ButtonStyle( @@ -140,8 +140,9 @@ class _AppDrawerState extends State { runSpacing: 8, children: [ OutlinedButton.icon( + key: const Key('drawer-about-button'), onPressed: () => goTo(AboutPage.routeName, (_) => AboutPage()), - icon: Icon(AIcons.info), + icon: const Icon(AIcons.info), label: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -155,11 +156,11 @@ class _AppDrawerState extends State { duration: Durations.newsBadgeAnimation, opacity: newVersion ? 1 : 0, child: Padding( - padding: EdgeInsetsDirectional.only(start: 2), + padding: const EdgeInsetsDirectional.only(start: 2), child: DecoratedBox( decoration: BoxDecoration( - border: Border.all(color: Colors.white70), - borderRadius: BorderRadius.circular(badgeSize), + border: const Border.fromBorderSide(BorderSide(color: Colors.white70)), + borderRadius: BorderRadius.all(Radius.circular(badgeSize)), ), child: Icon( Icons.circle, @@ -175,8 +176,9 @@ class _AppDrawerState extends State { ), ), OutlinedButton.icon( + key: const Key('drawer-settings-button'), onPressed: () => goTo(SettingsPage.routeName, (_) => SettingsPage()), - icon: Icon(AIcons.settings), + icon: const Icon(AIcons.settings), label: Text(context.l10n.settingsPageTitle), ), ], @@ -198,10 +200,10 @@ class _AppDrawerState extends State { }).toList() ..sort(source.compareAlbumsByName); - if (specialAlbums.isEmpty) return SizedBox.shrink(); + if (specialAlbums.isEmpty) return const SizedBox.shrink(); return Column( children: [ - Divider(), + const Divider(), ...specialAlbums.map((album) => AlbumTile(album)), ], ); @@ -211,19 +213,19 @@ class _AppDrawerState extends State { // tiles Widget get allCollectionTile => CollectionNavTile( - leading: Icon(AIcons.allCollection), + leading: const Icon(AIcons.allCollection), title: context.l10n.drawerCollectionAll, filter: null, ); Widget get videoTile => CollectionNavTile( - leading: Icon(AIcons.video), + leading: const Icon(AIcons.video), title: context.l10n.drawerCollectionVideos, filter: MimeFilter.video, ); Widget get favouriteTile => CollectionNavTile( - leading: Icon(AIcons.favourite), + leading: const Icon(AIcons.favourite), title: context.l10n.drawerCollectionFavourites, filter: FavouriteFilter.instance, ); diff --git a/lib/widgets/drawer/collection_tile.dart b/lib/widgets/drawer/collection_tile.dart index 5001baa48..667f92d85 100644 --- a/lib/widgets/drawer/collection_tile.dart +++ b/lib/widgets/drawer/collection_tile.dart @@ -2,23 +2,22 @@ import 'package:aves/model/filters/filters.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:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class CollectionNavTile extends StatelessWidget { - final Widget leading; + final Widget? leading; final String title; - final Widget trailing; + final Widget? trailing; final bool dense; - final CollectionFilter filter; + final CollectionFilter? filter; const CollectionNavTile({ - @required this.leading, - @required this.title, + required this.leading, + required this.title, this.trailing, - bool dense, - @required this.filter, + bool? dense, + required this.filter, }) : dense = dense ?? false; @override @@ -41,7 +40,7 @@ class CollectionNavTile extends StatelessWidget { Navigator.pushAndRemoveUntil( context, MaterialPageRoute( - settings: RouteSettings(name: CollectionPage.routeName), + settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage(CollectionLens( source: context.read(), filters: [filter], diff --git a/lib/widgets/drawer/tile.dart b/lib/widgets/drawer/tile.dart index 47602c4fd..5fce6f524 100644 --- a/lib/widgets/drawer/tile.dart +++ b/lib/widgets/drawer/tile.dart @@ -5,18 +5,18 @@ import 'package:flutter/material.dart'; class NavTile extends StatelessWidget { final IconData icon; final String title; - final Widget trailing; + final Widget? trailing; final bool topLevel; final String routeName; final WidgetBuilder pageBuilder; const NavTile({ - @required this.icon, - @required this.title, + required this.icon, + required this.title, this.trailing, this.topLevel = true, - @required this.routeName, - @required this.pageBuilder, + required this.routeName, + required this.pageBuilder, }); @override @@ -32,9 +32,9 @@ class NavTile extends StatelessWidget { ? Builder( builder: (context) => DefaultTextStyle.merge( style: TextStyle( - color: IconTheme.of(context).color.withOpacity(.6), + color: IconTheme.of(context).color!.withOpacity(.6), ), - child: trailing, + child: trailing!, ), ) : null, diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index 4c44049d6..5f9b714c1 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -30,8 +30,8 @@ class AlbumPickPage extends StatefulWidget { final MoveType moveType; const AlbumPickPage({ - @required this.source, - @required this.moveType, + required this.source, + required this.moveType, }); @override @@ -66,15 +66,15 @@ class _AlbumPickPageState extends State { showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, queryNotifier: _queryNotifier, applyQuery: (filters, query) { - if (query == null || query.isEmpty) return filters; + if (query.isEmpty) return filters; query = query.toUpperCase(); - return filters.where((item) => item.filter.displayName.toUpperCase().contains(query)).toList(); + return filters.where((item) => (item.filter.displayName ?? item.filter.album).toUpperCase().contains(query)).toList(); }, emptyBuilder: () => EmptyContent( icon: AIcons.album, text: context.l10n.albumEmpty, ), - onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter)?.album), + onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter).album), ), ); }, @@ -91,10 +91,10 @@ class AlbumPickAppBar extends StatelessWidget { static const preferredHeight = kToolbarHeight + AlbumFilterBar.preferredHeight; const AlbumPickAppBar({ - @required this.source, - @required this.moveType, - @required this.actionDelegate, - @required this.queryNotifier, + required this.source, + required this.moveType, + required this.actionDelegate, + required this.queryNotifier, }); @override @@ -108,12 +108,12 @@ class AlbumPickAppBar extends StatelessWidget { case MoveType.move: return context.l10n.albumPickPageTitleMove; default: - return null; + return moveType.toString(); } } return SliverAppBar( - leading: BackButton(), + leading: const BackButton(), title: SourceStateAwareAppBarTitle( title: Text(title()), source: source, @@ -123,7 +123,7 @@ class AlbumPickAppBar extends StatelessWidget { ), actions: [ IconButton( - icon: Icon(AIcons.createAlbum), + icon: const Icon(AIcons.createAlbum), onPressed: () async { final newAlbum = await showDialog( context: context, @@ -169,11 +169,11 @@ class AlbumFilterBar extends StatelessWidget implements PreferredSizeWidget { static const preferredHeight = kToolbarHeight; const AlbumFilterBar({ - @required this.filterNotifier, + required this.filterNotifier, }); @override - Size get preferredSize => Size.fromHeight(preferredHeight); + Size get preferredSize => const Size.fromHeight(preferredHeight); @override Widget build(BuildContext context) { diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index 9e5049b99..e640c50bf 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -75,8 +75,8 @@ class AlbumListPage extends StatelessWidget { static Map>> _group(BuildContext context, Iterable> sortedMapEntries) { final pinned = settings.pinnedFilters.whereType(); final byPin = groupBy, bool>(sortedMapEntries, (e) => pinned.contains(e.filter)); - final pinnedMapEntries = (byPin[true] ?? []); - final unpinnedMapEntries = (byPin[false] ?? []); + final pinnedMapEntries = byPin[true] ?? []; + final unpinnedMapEntries = byPin[false] ?? []; var sections = >>{}; switch (settings.albumGroupFactor) { @@ -94,11 +94,12 @@ class AlbumListPage extends StatelessWidget { return specialKey; } }); - sections = { + sections = Map.fromEntries({ + // group ordering specialKey: sections[specialKey], appsKey: sections[appsKey], regularKey: sections[regularKey], - }..removeWhere((key, value) => value == null); + }.entries.where((kv) => kv.value != null).cast>>>()); break; case AlbumChipGroupFactor.volume: sections = groupBy, ChipSectionKey>(unpinnedMapEntries, (kv) { @@ -108,7 +109,7 @@ class AlbumListPage extends StatelessWidget { case AlbumChipGroupFactor.none: return { if (pinnedMapEntries.isNotEmpty || unpinnedMapEntries.isNotEmpty) - ChipSectionKey(): [ + const ChipSectionKey(): [ ...pinnedMapEntries, ...unpinnedMapEntries, ], diff --git a/lib/widgets/filter_grids/common/chip_action_delegate.dart b/lib/widgets/filter_grids/common/chip_action_delegate.dart index 69ebb2de8..5b0cf1143 100644 --- a/lib/widgets/filter_grids/common/chip_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_action_delegate.dart @@ -22,6 +22,7 @@ import 'package:aves/widgets/dialogs/rename_album_dialog.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:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -83,8 +84,8 @@ class ChipActionDelegate { void _showCoverSelectionDialog(BuildContext context, CollectionFilter filter) async { final contentId = covers.coverContentId(filter); - final customEntry = context.read().visibleEntries.firstWhere((entry) => entry.contentId == contentId, orElse: () => null); - final coverSelection = await showDialog>( + final customEntry = context.read().visibleEntries.firstWhereOrNull((entry) => entry.contentId == contentId); + final coverSelection = await showDialog>( context: context, builder: (context) => CoverSelectionDialog( filter: filter, diff --git a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart index c9081d0e4..137a87e81 100644 --- a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart @@ -53,7 +53,7 @@ abstract class ChipSetActionDelegate { Navigator.push( context, MaterialPageRoute( - settings: RouteSettings(name: StatsPage.routeName), + settings: const RouteSettings(name: StatsPage.routeName), builder: (context) => StatsPage( source: source, ), diff --git a/lib/widgets/filter_grids/common/decorated_filter_chip.dart b/lib/widgets/filter_grids/common/decorated_filter_chip.dart index 841745d44..5ff7ffe83 100644 --- a/lib/widgets/filter_grids/common/decorated_filter_chip.dart +++ b/lib/widgets/filter_grids/common/decorated_filter_chip.dart @@ -24,22 +24,24 @@ import 'package:provider/provider.dart'; class DecoratedFilterChip extends StatelessWidget { final CollectionFilter filter; - final double extent; - final AvesEntry coverEntry; + final double extent, thumbnailExtent; + final AvesEntry? coverEntry; final bool pinned, highlightable; - final FilterCallback onTap; - final OffsetFilterCallback onLongPress; + final FilterCallback? onTap; + final OffsetFilterCallback? onLongPress; const DecoratedFilterChip({ - Key key, - @required this.filter, - @required this.extent, + Key? key, + required this.filter, + required this.extent, + double? thumbnailExtent, this.coverEntry, this.pinned = false, this.highlightable = true, this.onTap, this.onLongPress, - }) : super(key: key); + }) : thumbnailExtent = thumbnailExtent ?? extent, + super(key: key); @override Widget build(BuildContext context) { @@ -50,7 +52,7 @@ class DecoratedFilterChip extends StatelessWidget { { final album = (filter as AlbumFilter).album; return StreamBuilder( - stream: source.eventBus.on().where((event) => event.directories == null || event.directories.contains(album)), + stream: source.eventBus.on().where((event) => event.directories == null || event.directories!.contains(album)), builder: (context, snapshot) => _buildChip(source), ); } @@ -58,7 +60,7 @@ class DecoratedFilterChip extends StatelessWidget { { final countryCode = (filter as LocationFilter).countryCode; return StreamBuilder( - stream: source.eventBus.on().where((event) => event.countryCodes == null || event.countryCodes.contains(countryCode)), + stream: source.eventBus.on().where((event) => event.countryCodes == null || event.countryCodes!.contains(countryCode)), builder: (context, snapshot) => _buildChip(source), ); } @@ -66,17 +68,19 @@ class DecoratedFilterChip extends StatelessWidget { { final tag = (filter as TagFilter).tag; return StreamBuilder( - stream: source.eventBus.on().where((event) => event.tags == null || event.tags.contains(tag)), + stream: source.eventBus.on().where((event) => event.tags == null || event.tags!.contains(tag)), builder: (context, snapshot) => _buildChip(source), ); } default: - return SizedBox(); + return const SizedBox(); } }, ); } + static Radius radius(double extent) => Radius.circular(min(AvesFilterChip.defaultRadius, extent / 4)); + Widget _buildChip(CollectionSource source) { final entry = coverEntry ?? source.coverEntry(filter); final backgroundImage = entry == null @@ -88,11 +92,10 @@ class DecoratedFilterChip extends StatelessWidget { ) : RasterImageThumbnail( entry: entry, - extent: extent, + extent: thumbnailExtent, ); - final radius = min(AvesFilterChip.defaultRadius, extent / 4); final titlePadding = min(4.0, extent / 32); - final borderRadius = BorderRadius.circular(radius); + final borderRadius = BorderRadius.all(radius(extent)); Widget child = AvesFilterChip( filter: filter, showGenericIcon: false, @@ -140,7 +143,7 @@ class DecoratedFilterChip extends StatelessWidget { child: DecoratedIcon( AIcons.pin, color: FilterGridPage.detailColor, - shadows: [Constants.embossShadow], + shadows: Constants.embossShadows, size: iconSize, ), ), @@ -151,7 +154,7 @@ class DecoratedFilterChip extends StatelessWidget { child: DecoratedIcon( AIcons.removableStorage, color: FilterGridPage.detailColor, - shadows: [Constants.embossShadow], + shadows: Constants.embossShadows, size: iconSize, ), ), diff --git a/lib/widgets/filter_grids/common/draggable_thumb_label.dart b/lib/widgets/filter_grids/common/draggable_thumb_label.dart index ba1dab337..0967c385c 100644 --- a/lib/widgets/filter_grids/common/draggable_thumb_label.dart +++ b/lib/widgets/filter_grids/common/draggable_thumb_label.dart @@ -11,8 +11,8 @@ class FilterDraggableThumbLabel extends StatelessWid final double offsetY; const FilterDraggableThumbLabel({ - @required this.sortFactor, - @required this.offsetY, + required this.sortFactor, + required this.offsetY, }); @override @@ -25,17 +25,15 @@ class FilterDraggableThumbLabel extends StatelessWid return [ context.l10n.itemCount(context.read().count(filterGridItem.filter)), ]; - break; case ChipSortFactor.date: return [ - DraggableThumbLabel.formatMonthThumbLabel(context, filterGridItem.entry.bestDate), + DraggableThumbLabel.formatMonthThumbLabel(context, filterGridItem.entry?.bestDate), ]; case ChipSortFactor.name: return [ filterGridItem.filter.getLabel(context), ]; } - return []; }, ); } diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index af4f98381..3ddc72ab8 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -11,6 +11,7 @@ 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/extensions/media_query.dart'; +import 'package:aves/widgets/common/grid/item_tracker.dart'; import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/sliver.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; @@ -24,6 +25,7 @@ import 'package:aves/widgets/filter_grids/common/decorated_filter_chip.dart'; import 'package:aves/widgets/filter_grids/common/draggable_thumb_label.dart'; import 'package:aves/widgets/filter_grids/common/section_keys.dart'; import 'package:aves/widgets/filter_grids/common/section_layout.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -33,30 +35,30 @@ import 'package:tuple/tuple.dart'; typedef QueryTest = Iterable> Function(Iterable> filters, String query); class FilterGridPage extends StatelessWidget { - final String settingsRouteKey; + final String? settingsRouteKey; final Widget appBar; final double appBarHeight; final Map>> filterSections; final ChipSortFactor sortFactor; final bool showHeaders; final ValueNotifier queryNotifier; - final QueryTest applyQuery; + final QueryTest? applyQuery; final Widget Function() emptyBuilder; final FilterCallback onTap; - final OffsetFilterCallback onLongPress; + final OffsetFilterCallback? onLongPress; const FilterGridPage({ - Key key, + Key? key, this.settingsRouteKey, - @required this.appBar, + required this.appBar, this.appBarHeight = kToolbarHeight, - @required this.filterSections, - @required this.sortFactor, - @required this.showHeaders, - @required this.queryNotifier, + required this.filterSections, + required this.sortFactor, + required this.showHeaders, + required this.queryNotifier, this.applyQuery, - @required this.emptyBuilder, - @required this.onTap, + required this.emptyBuilder, + required this.onTap, this.onLongPress, }) : super(key: key); @@ -97,31 +99,31 @@ class FilterGridPage extends StatelessWidget { } class FilterGrid extends StatefulWidget { - final String settingsRouteKey; + final String? settingsRouteKey; final Widget appBar; final double appBarHeight; final Map>> filterSections; final ChipSortFactor sortFactor; final bool showHeaders; final ValueNotifier queryNotifier; - final QueryTest applyQuery; + final QueryTest? applyQuery; final Widget Function() emptyBuilder; final FilterCallback onTap; - final OffsetFilterCallback onLongPress; + final OffsetFilterCallback? onLongPress; const FilterGrid({ - Key key, - @required this.settingsRouteKey, - @required this.appBar, - @required this.appBarHeight, - @required this.filterSections, - @required this.sortFactor, - @required this.showHeaders, - @required this.queryNotifier, - @required this.applyQuery, - @required this.emptyBuilder, - @required this.onTap, - @required this.onLongPress, + Key? key, + required this.settingsRouteKey, + required this.appBar, + required this.appBarHeight, + required this.filterSections, + required this.sortFactor, + required this.showHeaders, + required this.queryNotifier, + required this.applyQuery, + required this.emptyBuilder, + required this.onTap, + required this.onLongPress, }) : super(key: key); @override @@ -129,18 +131,18 @@ class FilterGrid extends StatefulWidget { } class _FilterGridState extends State> { - TileExtentController _tileExtentController; + TileExtentController? _tileExtentController; @override Widget build(BuildContext context) { _tileExtentController ??= TileExtentController( - settingsRouteKey: widget.settingsRouteKey ?? context.currentRouteName, + settingsRouteKey: widget.settingsRouteKey ?? context.currentRouteName!, columnCountDefault: 2, extentMin: 60, spacing: 8, ); return TileExtentControllerProvider( - controller: _tileExtentController, + controller: _tileExtentController!, child: _FilterGridContent( appBar: widget.appBar, appBarHeight: widget.appBarHeight, @@ -164,24 +166,24 @@ class _FilterGridContent extends StatelessWidget { final bool showHeaders; final ValueNotifier queryNotifier; final Widget Function() emptyBuilder; - final QueryTest applyQuery; + final QueryTest? applyQuery; final FilterCallback onTap; - final OffsetFilterCallback onLongPress; + final OffsetFilterCallback? onLongPress; final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); _FilterGridContent({ - Key key, - @required this.appBar, - @required double appBarHeight, - @required this.filterSections, - @required this.sortFactor, - @required this.showHeaders, - @required this.queryNotifier, - @required this.applyQuery, - @required this.emptyBuilder, - @required this.onTap, - @required this.onLongPress, + Key? key, + required this.appBar, + required double appBarHeight, + required this.filterSections, + required this.sortFactor, + required this.showHeaders, + required this.queryNotifier, + required this.applyQuery, + required this.emptyBuilder, + required this.onTap, + required this.onLongPress, }) : super(key: key) { _appBarHeightNotifier.value = appBarHeight; } @@ -197,7 +199,7 @@ class _FilterGridContent extends StatelessWidget { } else { visibleFilterSections = {}; filterSections.forEach((sectionKey, sectionFilters) { - final visibleFilters = applyQuery(sectionFilters, query); + final visibleFilters = applyQuery!(sectionFilters, query); if (visibleFilters.isNotEmpty) { visibleFilterSections[sectionKey] = visibleFilters.toList(); } @@ -221,9 +223,9 @@ class _FilterGridContent extends StatelessWidget { sections: visibleFilterSections, showHeaders: showHeaders, scrollableWidth: scrollableWidth, - tileExtent: tileExtent, columnCount: columnCount, spacing: tileSpacing, + tileExtent: tileExtent, tileBuilder: (gridItem) { final filter = gridItem.filter; final entry = gridItem.entry; @@ -246,7 +248,7 @@ class _FilterGridContent extends StatelessWidget { visibleFilterSections: visibleFilterSections, sortFactor: sortFactor, emptyBuilder: emptyBuilder, - scrollController: PrimaryScrollController.of(context), + scrollController: PrimaryScrollController.of(context)!, ), ); }); @@ -267,42 +269,45 @@ class _FilterSectionedContent extends StatefulWidget final ScrollController scrollController; const _FilterSectionedContent({ - @required this.appBar, - @required this.appBarHeightNotifier, - @required this.visibleFilterSections, - @required this.sortFactor, - @required this.emptyBuilder, - @required this.scrollController, + required this.appBar, + required this.appBarHeightNotifier, + required this.visibleFilterSections, + required this.sortFactor, + required this.emptyBuilder, + required this.scrollController, }); @override _FilterSectionedContentState createState() => _FilterSectionedContentState(); } -class _FilterSectionedContentState extends State<_FilterSectionedContent> { +class _FilterSectionedContentState extends State<_FilterSectionedContent> with WidgetsBindingObserver, GridItemTrackerMixin, _FilterSectionedContent> { Widget get appBar => widget.appBar; + @override ValueNotifier get appBarHeightNotifier => widget.appBarHeightNotifier; Map>> get visibleFilterSections => widget.visibleFilterSections; Widget Function() get emptyBuilder => widget.emptyBuilder; + @override ScrollController get scrollController => widget.scrollController; - final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'filter-grid-page-scrollable'); + @override + final GlobalKey scrollableKey = GlobalKey(debugLabel: 'filter-grid-page-scrollable'); @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => _checkInitHighlight()); + WidgetsBinding.instance!.addPostFrameCallback((_) => _checkInitHighlight()); } @override Widget build(BuildContext context) { final scrollView = AnimationLimiter( child: _FilterScrollView( - scrollableKey: _scrollableKey, + scrollableKey: scrollableKey, appBar: appBar, appBarHeightNotifier: appBarHeightNotifier, sortFactor: widget.sortFactor, @@ -312,7 +317,7 @@ class _FilterSectionedContentState extends State<_Fi ); final scaler = _FilterScaler( - scrollableKey: _scrollableKey, + scrollableKey: scrollableKey, appBarHeightNotifier: appBarHeightNotifier, child: scrollView, ); @@ -324,34 +329,13 @@ class _FilterSectionedContentState extends State<_Fi final highlightInfo = context.read(); final filter = highlightInfo.clear(); if (filter is T) { - final gridItem = visibleFilterSections.values.expand((list) => list).firstWhere((gridItem) => gridItem.filter == filter, orElse: () => null); + final gridItem = visibleFilterSections.values.expand((list) => list).firstWhereOrNull((gridItem) => gridItem.filter == filter); if (gridItem != null) { await Future.delayed(Durations.highlightScrollInitDelay); - final sectionedListLayout = context.read>>(); - final tileRect = sectionedListLayout.getTileRect(gridItem); - await _scrollToItem(tileRect); - highlightInfo.set(filter); + highlightInfo.trackItem(gridItem, highlightItem: filter); } } } - - Future _scrollToItem(Rect tileRect) async { - final scrollableContext = _scrollableKey.currentContext; - final scrollableHeight = (scrollableContext.findRenderObject() as RenderBox).size.height; - - // most of the time the app bar will be scrolled away after scaling, - // so we compensate for it to center the focal point thumbnail - final appBarHeight = appBarHeightNotifier.value; - final scrollOffset = tileRect.top + (tileRect.height - scrollableHeight) / 2 + appBarHeight; - - if (scrollOffset > 0) { - await scrollController.animateTo( - scrollOffset, - duration: Duration(milliseconds: (scrollOffset / 2).round().clamp(Durations.highlightScrollAnimationMinMillis, Durations.highlightScrollAnimationMaxMillis)), - curve: Curves.easeInOutCubic, - ); - } - } } class _FilterScaler extends StatelessWidget { @@ -360,9 +344,9 @@ class _FilterScaler extends StatelessWidget { final Widget child; const _FilterScaler({ - @required this.scrollableKey, - @required this.appBarHeightNotifier, - @required this.child, + required this.scrollableKey, + required this.appBarHeightNotifier, + required this.child, }); @override @@ -371,12 +355,13 @@ class _FilterScaler extends StatelessWidget { final tileSpacing = context.select((controller) => controller.spacing); return GridScaleGestureDetector>( scrollableKey: scrollableKey, - appBarHeightNotifier: appBarHeightNotifier, gridBuilder: (center, extent, child) => CustomPaint( painter: GridPainter( center: center, extent: extent, spacing: tileSpacing, + borderWidth: AvesFilterChip.outlineWidth, + borderRadius: DecoratedFilterChip.radius(extent), color: Colors.grey.shade700, ), child: child, @@ -386,15 +371,12 @@ class _FilterScaler extends StatelessWidget { return DecoratedFilterChip( filter: filter, extent: extent, + thumbnailExtent: context.read().effectiveExtentMax, pinned: pinnedFilters.contains(filter), highlightable: false, ); }, - getScaledItemTileRect: (context, item) { - final sectionedListLayout = context.read>>(); - return sectionedListLayout.getTileRect(item) ?? Rect.zero; - }, - onScaled: (item) => context.read().set(item.filter), + highlightItem: (item) => item.filter, child: child, ); } @@ -409,12 +391,12 @@ class _FilterScrollView extends StatelessWidget { final ScrollController scrollController; const _FilterScrollView({ - @required this.scrollableKey, - @required this.appBar, - @required this.appBarHeightNotifier, - @required this.sortFactor, - @required this.emptyBuilder, - @required this.scrollController, + required this.scrollableKey, + required this.appBar, + required this.appBarHeightNotifier, + required this.sortFactor, + required this.emptyBuilder, + required this.scrollController, }); @override diff --git a/lib/widgets/filter_grids/common/filter_nav_page.dart b/lib/widgets/filter_grids/common/filter_nav_page.dart index 4c8a29fe6..c5dbb7499 100644 --- a/lib/widgets/filter_grids/common/filter_nav_page.dart +++ b/lib/widgets/filter_grids/common/filter_nav_page.dart @@ -13,6 +13,7 @@ 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_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/filter_grids/common/chip_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; @@ -36,23 +37,23 @@ class FilterNavigationPage extends StatelessWidget { final Widget Function() emptyBuilder; const FilterNavigationPage({ - @required this.source, - @required this.title, - @required this.sortFactor, + required this.source, + required this.title, + required this.sortFactor, this.groupable = false, this.showHeaders = false, - @required this.chipSetActionDelegate, - @required this.chipActionDelegate, - @required this.chipActionsBuilder, - @required this.filterSections, - @required this.emptyBuilder, + required this.chipSetActionDelegate, + required this.chipActionDelegate, + required this.chipActionsBuilder, + required this.filterSections, + required this.emptyBuilder, }); @override Widget build(BuildContext context) { final isMainMode = context.select, bool>((vn) => vn.value == AppMode.main); return FilterGridPage( - key: ValueKey('filter-grid-page'), + key: const Key('filter-grid-page'), appBar: SliverAppBar( title: InteractiveAppBarTitle( onTap: () => _goToSearch(context), @@ -72,29 +73,29 @@ class FilterNavigationPage extends StatelessWidget { emptyBuilder: () => ValueListenableBuilder( valueListenable: source.stateNotifier, builder: (context, sourceState, child) { - return sourceState != SourceState.loading && emptyBuilder != null ? emptyBuilder() : SizedBox.shrink(); + return sourceState != SourceState.loading ? emptyBuilder() : const SizedBox.shrink(); }, ), onTap: (filter) => Navigator.push( context, MaterialPageRoute( - settings: RouteSettings(name: CollectionPage.routeName), + settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage(CollectionLens( source: source, filters: [filter], )), ), ), - onLongPress: isMainMode ? _showMenu : null, + onLongPress: isMainMode ? _showMenu as OffsetFilterCallback : null, ); } - void _showMenu(BuildContext context, T filter, Offset tapPosition) async { - final RenderBox overlay = Overlay.of(context).context.findRenderObject(); - final touchArea = Size(40, 40); + void _showMenu(BuildContext context, T filter, Offset? tapPosition) async { + final overlay = Overlay.of(context)!.context.findRenderObject() as RenderBox; + const touchArea = Size(40, 40); final selectedAction = await showMenu( context: context, - position: RelativeRect.fromRect(tapPosition & touchArea, Offset.zero & overlay.size), + position: RelativeRect.fromRect((tapPosition ?? Offset.zero) & touchArea, Offset.zero & overlay.size), items: chipActionsBuilder(filter) .map((action) => PopupMenuItem( value: action, @@ -112,11 +113,11 @@ class FilterNavigationPage extends StatelessWidget { return [ CollectionSearchButton(source), PopupMenuButton( - key: Key('appbar-menu-button'), + key: const Key('appbar-menu-button'), itemBuilder: (context) { return [ PopupMenuItem( - key: Key('menu-sort'), + key: const Key('menu-sort'), value: ChipSetAction.sort, child: MenuRow(text: context.l10n.menuActionSort, icon: AIcons.sort), ), @@ -155,7 +156,7 @@ class FilterNavigationPage extends StatelessWidget { } static int compareFiltersByEntryCount(MapEntry a, MapEntry b) { - final c = b.value.compareTo(a.value) ?? -1; + final c = b.value.compareTo(a.value); return c != 0 ? c : a.key.compareTo(b.key); } @@ -164,14 +165,14 @@ class FilterNavigationPage extends StatelessWidget { } static Iterable> sort(ChipSortFactor sortFactor, CollectionSource source, Set filters) { - Iterable> toGridItem(CollectionSource source, Iterable filters) { + Iterable> toGridItem(CollectionSource source, Set filters) { return filters.map((filter) => FilterGridItem( filter, source.recentEntry(filter), )); } - Iterable> allMapEntries; + Iterable> allMapEntries = {}; switch (sortFactor) { case ChipSortFactor.name: allMapEntries = toGridItem(source, filters).toList()..sort(compareFiltersByName); diff --git a/lib/widgets/filter_grids/common/overlay.dart b/lib/widgets/filter_grids/common/overlay.dart index 01914443a..7c2322ee0 100644 --- a/lib/widgets/filter_grids/common/overlay.dart +++ b/lib/widgets/filter_grids/common/overlay.dart @@ -12,10 +12,10 @@ class ChipHighlightOverlay extends StatefulWidget { final BorderRadius borderRadius; const ChipHighlightOverlay({ - Key key, - @required this.filter, - @required this.extent, - @required this.borderRadius, + Key? key, + required this.filter, + required this.extent, + required this.borderRadius, }) : super(key: key); @override @@ -34,10 +34,10 @@ class _ChipHighlightOverlayState extends State { return Sweeper( builder: (context) => Container( decoration: BoxDecoration( - border: Border.all( + border: Border.fromBorderSide(BorderSide( color: Theme.of(context).accentColor, width: widget.extent * .1, - ), + )), borderRadius: widget.borderRadius, ), ), diff --git a/lib/widgets/filter_grids/common/section_header.dart b/lib/widgets/filter_grids/common/section_header.dart index 90dab210f..b0038fa60 100644 --- a/lib/widgets/filter_grids/common/section_header.dart +++ b/lib/widgets/filter_grids/common/section_header.dart @@ -6,8 +6,8 @@ class FilterChipSectionHeader extends StatelessWidget { final ChipSectionKey sectionKey; const FilterChipSectionHeader({ - Key key, - @required this.sectionKey, + Key? key, + required this.sectionKey, }) : super(key: key); @override diff --git a/lib/widgets/filter_grids/common/section_keys.dart b/lib/widgets/filter_grids/common/section_keys.dart index 2ba69df8a..5e7c661b2 100644 --- a/lib/widgets/filter_grids/common/section_keys.dart +++ b/lib/widgets/filter_grids/common/section_keys.dart @@ -12,7 +12,7 @@ class ChipSectionKey extends SectionKey { this.title = '', }); - Widget get leading => null; + Widget? get leading => null; @override bool operator ==(Object other) { @@ -58,7 +58,6 @@ extension ExtraAlbumImportance on AlbumImportance { case AlbumImportance.regular: return context.l10n.albumTierRegular; } - return null; } IconData getIcon() { @@ -72,15 +71,14 @@ extension ExtraAlbumImportance on AlbumImportance { case AlbumImportance.regular: return AIcons.album; } - return null; } } class StorageVolumeSectionKey extends ChipSectionKey { - final StorageVolume volume; + final StorageVolume? volume; StorageVolumeSectionKey(BuildContext context, this.volume) : super(title: volume?.getDescription(context) ?? context.l10n.sectionUnknown); @override - Widget get leading => (volume?.isRemovable ?? false) ? Icon(AIcons.removableStorage) : null; + Widget? get leading => (volume?.isRemovable ?? false) ? const Icon(AIcons.removableStorage) : null; } diff --git a/lib/widgets/filter_grids/common/section_layout.dart b/lib/widgets/filter_grids/common/section_layout.dart index 154bcc401..ab9f1e091 100644 --- a/lib/widgets/filter_grids/common/section_layout.dart +++ b/lib/widgets/filter_grids/common/section_layout.dart @@ -2,20 +2,20 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/filter_grids/common/section_header.dart'; -import 'package:flutter/foundation.dart'; +import 'package:aves/widgets/filter_grids/common/section_keys.dart'; import 'package:flutter/material.dart'; class SectionedFilterListLayoutProvider extends SectionedListLayoutProvider> { const SectionedFilterListLayoutProvider({ - @required this.sections, - @required this.showHeaders, - @required double scrollableWidth, - @required int columnCount, - double spacing = 0, - @required double tileExtent, - @required Widget Function(FilterGridItem gridItem) tileBuilder, - @required Duration tileAnimationDelay, - @required Widget child, + required this.sections, + required this.showHeaders, + required double scrollableWidth, + required int columnCount, + required double spacing, + required double tileExtent, + required Widget Function(FilterGridItem gridItem) tileBuilder, + required Duration tileAnimationDelay, + required Widget child, }) : super( scrollableWidth: scrollableWidth, columnCount: columnCount, @@ -40,7 +40,7 @@ class SectionedFilterListLayoutProvider extends Sect @override Widget buildHeader(BuildContext context, SectionKey sectionKey, double headerExtent) { return FilterChipSectionHeader( - sectionKey: sectionKey, + sectionKey: sectionKey as ChipSectionKey, ); } } diff --git a/lib/widgets/filter_grids/countries_page.dart b/lib/widgets/filter_grids/countries_page.dart index 087e00c5e..a759cfcea 100644 --- a/lib/widgets/filter_grids/countries_page.dart +++ b/lib/widgets/filter_grids/countries_page.dart @@ -65,7 +65,7 @@ class CountryListPage extends StatelessWidget { return { if (pinnedMapEntries.isNotEmpty || unpinnedMapEntries.isNotEmpty) - ChipSectionKey(): [ + const ChipSectionKey(): [ ...pinnedMapEntries, ...unpinnedMapEntries, ], diff --git a/lib/widgets/filter_grids/tags_page.dart b/lib/widgets/filter_grids/tags_page.dart index 6b0f86520..b6f05e5c1 100644 --- a/lib/widgets/filter_grids/tags_page.dart +++ b/lib/widgets/filter_grids/tags_page.dart @@ -65,7 +65,7 @@ class TagListPage extends StatelessWidget { return { if (pinnedMapEntries.isNotEmpty || unpinnedMapEntries.isNotEmpty) - ChipSectionKey(): [ + const ChipSectionKey(): [ ...pinnedMapEntries, ...unpinnedMapEntries, ], diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 3b1c56f1b..8acb9c8d8 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -26,7 +26,7 @@ class HomePage extends StatefulWidget { static const routeName = '/'; // untyped map as it is coming from the platform - final Map intentData; + final Map? intentData; const HomePage({this.intentData}); @@ -35,9 +35,9 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - AvesEntry _viewerEntry; - String _shortcutRouteName; - List _shortcutFilters; + AvesEntry? _viewerEntry; + String? _shortcutRouteName; + List? _shortcutFilters; static const allowedShortcutRoutes = [CollectionPage.routeName, AlbumListPage.routeName, SearchPage.routeName]; @@ -45,12 +45,12 @@ class _HomePageState extends State { void initState() { super.initState(); _setup(); - imageCache.maximumSizeBytes = 512 * (1 << 20); + imageCache!.maximumSizeBytes = 512 * (1 << 20); settings.keepScreenOn.apply(); } @override - Widget build(BuildContext context) => Scaffold(); + Widget build(BuildContext context) => const Scaffold(); Future _setup() async { final permissions = await [ @@ -68,7 +68,7 @@ class _HomePageState extends State { var appMode = AppMode.main; final intentData = widget.intentData ?? await ViewerService.getIntentData(); - if (intentData?.isNotEmpty == true) { + if (intentData.isNotEmpty) { final action = intentData['action']; switch (action) { case 'view': @@ -84,7 +84,7 @@ class _HomePageState extends State { appMode = AppMode.pickExternal; // TODO TLAD apply pick mimetype(s) // some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?) - String pickMimeTypes = intentData['mimeType']; + String? pickMimeTypes = intentData['mimeType']; debugPrint('pick mimeType=$pickMimeTypes'); break; default: @@ -115,7 +115,7 @@ class _HomePageState extends State { )); } - Future _initViewerEntry({@required String uri, @required String mimeType}) async { + Future _initViewerEntry({required String uri, required String? mimeType}) async { final entry = await imageFileService.getEntry(uri, mimeType); if (entry != null) { // cataloguing is essential for coordinates and video rotation @@ -127,15 +127,15 @@ class _HomePageState extends State { Route _getRedirectRoute(AppMode appMode) { if (appMode == AppMode.view) { return DirectMaterialPageRoute( - settings: RouteSettings(name: EntryViewerPage.routeName), + settings: const RouteSettings(name: EntryViewerPage.routeName), builder: (_) => EntryViewerPage( - initialEntry: _viewerEntry, + initialEntry: _viewerEntry!, ), ); } String routeName; - Iterable filters; + Iterable? filters; if (appMode == AppMode.pickExternal) { routeName = CollectionPage.routeName; } else { @@ -146,7 +146,7 @@ class _HomePageState extends State { switch (routeName) { case AlbumListPage.routeName: return DirectMaterialPageRoute( - settings: RouteSettings(name: AlbumListPage.routeName), + settings: const RouteSettings(name: AlbumListPage.routeName), builder: (_) => AlbumListPage(), ); case SearchPage.routeName: @@ -156,7 +156,7 @@ class _HomePageState extends State { case CollectionPage.routeName: default: return DirectMaterialPageRoute( - settings: RouteSettings(name: CollectionPage.routeName), + settings: const RouteSettings(name: CollectionPage.routeName), builder: (_) => CollectionPage( CollectionLens( source: source, diff --git a/lib/widgets/search/expandable_filter_row.dart b/lib/widgets/search/expandable_filter_row.dart index 07eba3dc2..28ae3a10c 100644 --- a/lib/widgets/search/expandable_filter_row.dart +++ b/lib/widgets/search/expandable_filter_row.dart @@ -6,18 +6,18 @@ import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:flutter/material.dart'; class ExpandableFilterRow extends StatelessWidget { - final String title; + final String? title; final Iterable filters; - final ValueNotifier expandedNotifier; - final HeroType Function(CollectionFilter filter) heroTypeBuilder; + final ValueNotifier expandedNotifier; + final HeroType Function(CollectionFilter filter)? heroTypeBuilder; final FilterCallback onTap; const ExpandableFilterRow({ this.title, - @required this.filters, - @required this.expandedNotifier, + required this.filters, + required this.expandedNotifier, this.heroTypeBuilder, - @required this.onTap, + required this.onTap, }); static const double horizontalPadding = 8; @@ -25,20 +25,20 @@ class ExpandableFilterRow extends StatelessWidget { @override Widget build(BuildContext context) { - if (filters.isEmpty) return SizedBox.shrink(); + if (filters.isEmpty) return const SizedBox.shrink(); - final hasTitle = title != null && title.isNotEmpty; + final hasTitle = title != null && title!.isNotEmpty; final isExpanded = hasTitle && expandedNotifier.value == title; - Widget titleRow; + Widget? titleRow; if (hasTitle) { titleRow = Padding( - padding: EdgeInsets.all(16), + padding: const EdgeInsets.all(16), child: Row( children: [ Text( - title, + title!, style: Constants.titleTextStyle, ), const Spacer(), @@ -55,7 +55,7 @@ class ExpandableFilterRow extends StatelessWidget { final filterList = filters.toList(); final wrap = Container( key: ValueKey('wrap$title'), - padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + padding: const EdgeInsets.symmetric(horizontal: horizontalPadding), // specify transparent as a workaround to prevent // chip border clipping when the floating app bar is fading color: Colors.transparent, @@ -73,12 +73,12 @@ class ExpandableFilterRow extends StatelessWidget { height: AvesFilterChip.minChipHeight, child: ListView.separated( scrollDirection: Axis.horizontal, - physics: BouncingScrollPhysics(), - padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: horizontalPadding), itemBuilder: (context, index) { - return index < filterList.length ? _buildFilterChip(filterList[index]) : null; + return index < filterList.length ? _buildFilterChip(filterList[index]) : const SizedBox(); }, - separatorBuilder: (context, index) => SizedBox(width: 8), + separatorBuilder: (context, index) => const SizedBox(width: 8), itemCount: filterList.length, ), ); diff --git a/lib/widgets/search/search_button.dart b/lib/widgets/search/search_button.dart index 3e377a670..21f37df81 100644 --- a/lib/widgets/search/search_button.dart +++ b/lib/widgets/search/search_button.dart @@ -6,15 +6,15 @@ import 'package:flutter/material.dart'; class CollectionSearchButton extends StatelessWidget { final CollectionSource source; - final CollectionLens parentCollection; + final CollectionLens? parentCollection; const CollectionSearchButton(this.source, {this.parentCollection}); @override Widget build(BuildContext context) { return IconButton( - key: Key('search-button'), - icon: Icon(AIcons.search), + key: const Key('search-button'), + icon: const Icon(AIcons.search), onPressed: () => _goToSearch(context), tooltip: MaterialLocalizations.of(context).searchFieldLabel, ); diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index f0f9adfc1..254ffc014 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -25,8 +25,8 @@ import 'package:provider/provider.dart'; class CollectionSearchDelegate { final CollectionSource source; - final CollectionLens parentCollection; - final ValueNotifier expandedSectionNotifier = ValueNotifier(null); + final CollectionLens? parentCollection; + final ValueNotifier expandedSectionNotifier = ValueNotifier(null); static const searchHistoryCount = 10; static final typeFilters = [ @@ -41,7 +41,7 @@ class CollectionSearchDelegate { MimeFilter(MimeTypes.svg), ]; - CollectionSearchDelegate({@required this.source, this.parentCollection}); + CollectionSearchDelegate({required this.source, this.parentCollection}); Widget buildLeading(BuildContext context) { return Navigator.canPop(context) @@ -53,7 +53,7 @@ class CollectionSearchDelegate { onPressed: () => _goBack(context), tooltip: MaterialLocalizations.of(context).backButtonTooltip, ) - : CloseButton( + : const CloseButton( onPressed: SystemNavigator.pop, ); } @@ -62,7 +62,7 @@ class CollectionSearchDelegate { return [ if (query.isNotEmpty) IconButton( - icon: Icon(AIcons.clear), + icon: const Icon(AIcons.clear), onPressed: () { query = ''; showSuggestions(context); @@ -76,7 +76,7 @@ class CollectionSearchDelegate { final upQuery = query.trim().toUpperCase(); bool containQuery(String s) => s.toUpperCase().contains(upQuery); return SafeArea( - child: ValueListenableBuilder( + child: ValueListenableBuilder( valueListenable: expandedSectionNotifier, builder: (context, expandedSection, child) { final queryFilter = _buildQueryFilter(false); @@ -93,14 +93,14 @@ class CollectionSearchDelegate { final history = settings.searchHistory.where(notHidden).toList(); return ListView( - padding: EdgeInsets.only(top: 8), + padding: const EdgeInsets.only(top: 8), children: [ _buildFilterRow( context: context, filters: [ queryFilter, ...visibleTypeFilters, - ].where((f) => f != null && containQuery(f.getLabel(context))).toList(), + ].where((f) => f != null && containQuery(f.getLabel(context))).cast().toList(), // usually perform hero animation only on tapped chips, // but we also need to animate the query chip when it is selected by submitting the search query heroTypeBuilder: (filter) => filter == queryFilter ? HeroType.always : HeroType.onTap, @@ -119,7 +119,7 @@ class CollectionSearchDelegate { album, source.getAlbumDisplayName(context, album), )) - .where((filter) => containQuery(filter.displayName)) + .where((filter) => containQuery(filter.displayName ?? filter.album)) .toList() ..sort(); return _buildFilterRow( @@ -174,10 +174,10 @@ class CollectionSearchDelegate { } Widget _buildFilterRow({ - @required BuildContext context, - String title, - @required List filters, - HeroType Function(CollectionFilter filter) heroTypeBuilder, + required BuildContext context, + String? title, + required List filters, + HeroType Function(CollectionFilter filter)? heroTypeBuilder, }) { return ExpandableFilterRow( title: title, @@ -189,21 +189,26 @@ class CollectionSearchDelegate { } 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 SizedBox.shrink(); + return const SizedBox.shrink(); } - QueryFilter _buildQueryFilter(bool colorful) { + QueryFilter? _buildQueryFilter(bool colorful) { final cleanQuery = query.trim(); return cleanQuery.isNotEmpty ? QueryFilter(cleanQuery, colorful: colorful) : null; } - void _select(BuildContext context, CollectionFilter filter) { + void _select(BuildContext context, CollectionFilter? filter) { + if (filter == null) { + _goBack(context); + return; + } + if (settings.saveSearchHistory) { final history = settings.searchHistory ..remove(filter) @@ -218,11 +223,11 @@ class CollectionSearchDelegate { } void _applyToParentCollectionPage(BuildContext context, CollectionFilter filter) { - parentCollection.addFilter(filter); + parentCollection!.addFilter(filter); // 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((_) { _goBack(context); }); } @@ -237,7 +242,7 @@ class CollectionSearchDelegate { Navigator.pushAndRemoveUntil( context, MaterialPageRoute( - settings: RouteSettings(name: CollectionPage.routeName), + settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage(CollectionLens( source: source, filters: [filter], @@ -261,13 +266,13 @@ class CollectionSearchDelegate { void showSuggestions(BuildContext context) { assert(focusNode != null, '_focusNode must be set by route before showSuggestions is called.'); - focusNode.requestFocus(); + focusNode!.requestFocus(); currentBody = SearchBody.suggestions; } Animation get transitionAnimation => proxyAnimation; - FocusNode focusNode; + FocusNode? focusNode; final TextEditingController queryTextController = TextEditingController(); @@ -276,19 +281,18 @@ class CollectionSearchDelegate { String get query => queryTextController.text; set query(String value) { - assert(query != null); queryTextController.text = value; } - final ValueNotifier currentBodyNotifier = ValueNotifier(null); + final ValueNotifier currentBodyNotifier = ValueNotifier(null); - SearchBody get currentBody => currentBodyNotifier.value; + SearchBody? get currentBody => currentBodyNotifier.value; - set currentBody(SearchBody value) { + set currentBody(SearchBody? value) { currentBodyNotifier.value = value; } - SearchPageRoute route; + SearchPageRoute? route; } // adapted from `SearchDelegate` @@ -297,9 +301,8 @@ enum SearchBody { suggestions, results } // adapted from `SearchDelegate` class SearchPageRoute extends PageRoute { SearchPageRoute({ - @required this.delegate, - }) : assert(delegate != null), - super(settings: RouteSettings(name: SearchPage.routeName)) { + required this.delegate, + }) : super(settings: const RouteSettings(name: SearchPage.routeName)) { assert( delegate.route == null, 'The ${delegate.runtimeType} instance is currently used by another active ' @@ -312,10 +315,10 @@ class SearchPageRoute extends PageRoute { final CollectionSearchDelegate delegate; @override - Color get barrierColor => null; + Color? get barrierColor => null; @override - String get barrierLabel => null; + String? get barrierLabel => null; @override Duration get transitionDuration => const Duration(milliseconds: 300); @@ -356,7 +359,7 @@ class SearchPageRoute extends PageRoute { } @override - void didComplete(T result) { + void didComplete(T? result) { super.didComplete(result); assert(delegate.route == this); delegate.route = null; diff --git a/lib/widgets/search/search_page.dart b/lib/widgets/search/search_page.dart index df0719d8b..534e0858a 100644 --- a/lib/widgets/search/search_page.dart +++ b/lib/widgets/search/search_page.dart @@ -12,8 +12,8 @@ class SearchPage extends StatefulWidget { final Animation animation; const SearchPage({ - @required this.delegate, - @required this.animation, + required this.delegate, + required this.animation, }); @override @@ -89,20 +89,22 @@ class _SearchPageState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - Widget body; + Widget? body; switch (widget.delegate.currentBody) { case SearchBody.suggestions: body = KeyedSubtree( - key: ValueKey(SearchBody.suggestions), + key: const ValueKey(SearchBody.suggestions), child: widget.delegate.buildSuggestions(context), ); break; case SearchBody.results: body = KeyedSubtree( - key: ValueKey(SearchBody.results), + key: const ValueKey(SearchBody.results), child: widget.delegate.buildResults(context), ); break; + case null: + break; } return Scaffold( appBar: AppBar( diff --git a/lib/widgets/settings/access_grants.dart b/lib/widgets/settings/access_grants.dart index ec0912290..23ae4a6dd 100644 --- a/lib/widgets/settings/access_grants.dart +++ b/lib/widgets/settings/access_grants.dart @@ -13,7 +13,7 @@ class StorageAccessTile extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - settings: RouteSettings(name: StorageAccessPage.routeName), + settings: const RouteSettings(name: StorageAccessPage.routeName), builder: (context) => StorageAccessPage(), ), ); @@ -30,8 +30,8 @@ class StorageAccessPage extends StatefulWidget { } class _StorageAccessPageState extends State { - Future> _pathLoader; - List _lastPaths; + late Future> _pathLoader; + List? _lastPaths; @override void initState() { @@ -52,16 +52,16 @@ class _StorageAccessPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Row( children: [ - Icon(AIcons.info), - SizedBox(width: 16), + const Icon(AIcons.info), + const SizedBox(width: 16), Expanded(child: Text(context.l10n.settingsStorageAccessBanner)), ], ), ), - Divider(), + const Divider(), Expanded( child: FutureBuilder>( future: _pathLoader, @@ -70,22 +70,22 @@ class _StorageAccessPageState extends State { return Text(snapshot.error.toString()); } if (snapshot.connectionState != ConnectionState.done && _lastPaths == null) { - return SizedBox.shrink(); + return const SizedBox.shrink(); } - _lastPaths = snapshot.data..sort(); - if (_lastPaths.isEmpty) { + _lastPaths = snapshot.data!..sort(); + if (_lastPaths!.isEmpty) { return EmptyContent( text: context.l10n.settingsStorageAccessEmpty, ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: _lastPaths + children: _lastPaths! .map((path) => ListTile( title: Text(path), dense: true, trailing: IconButton( - icon: Icon(AIcons.clear), + icon: const Icon(AIcons.clear), onPressed: () async { await storageService.revokeDirectoryAccess(path); _load(); diff --git a/lib/widgets/settings/entry_background.dart b/lib/widgets/settings/entry_background.dart index b0f1995f0..5f2c59ba6 100644 --- a/lib/widgets/settings/entry_background.dart +++ b/lib/widgets/settings/entry_background.dart @@ -9,8 +9,8 @@ class EntryBackgroundSelector extends StatefulWidget { final ValueSetter setter; const EntryBackgroundSelector({ - @required this.getter, - @required this.setter, + required this.getter, + required this.setter, }); @override @@ -25,8 +25,10 @@ class _EntryBackgroundSelectorState extends State { items: _buildItems(context), value: widget.getter(), onChanged: (selected) { - widget.setter(selected); - setState(() {}); + if (selected != null) { + widget.setter(selected); + setState(() {}); + } }, ), ); @@ -40,10 +42,10 @@ class _EntryBackgroundSelectorState extends State { EntryBackground.checkered, EntryBackground.transparent, ].map((selected) { - Widget child; + Widget? child; switch (selected) { case EntryBackground.transparent: - child = Icon( + child = const Icon( Icons.clear, size: 20, color: Colors.white30, @@ -68,7 +70,7 @@ class _EntryBackgroundSelectorState extends State { width: radius * 2, decoration: BoxDecoration( color: selected.isColor ? selected.color : null, - border: AvesCircleBorder.build(context), + border: AvesBorder.border, shape: BoxShape.circle, ), child: child, diff --git a/lib/widgets/settings/hidden_filters.dart b/lib/widgets/settings/hidden_filters.dart index 526b353f8..6e5a71f55 100644 --- a/lib/widgets/settings/hidden_filters.dart +++ b/lib/widgets/settings/hidden_filters.dart @@ -16,7 +16,7 @@ class HiddenFilterTile extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - settings: RouteSettings(name: HiddenFilterPage.routeName), + settings: const RouteSettings(name: HiddenFilterPage.routeName), builder: (context) => HiddenFilterPage(), ), ); @@ -39,19 +39,19 @@ class HiddenFilterPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Row( children: [ - Icon(AIcons.info), - SizedBox(width: 16), + const Icon(AIcons.info), + const SizedBox(width: 16), Expanded(child: Text(context.l10n.settingsHiddenFiltersBanner)), ], ), ), - Divider(), + const Divider(), Expanded( child: Padding( - padding: EdgeInsets.all(8), + padding: const EdgeInsets.all(8), child: Consumer( builder: (context, settings, child) { final hiddenFilters = settings.hiddenFilters; diff --git a/lib/widgets/settings/quick_actions/available_actions.dart b/lib/widgets/settings/quick_actions/available_actions.dart index e73891cb2..76ebd1aae 100644 --- a/lib/widgets/settings/quick_actions/available_actions.dart +++ b/lib/widgets/settings/quick_actions/available_actions.dart @@ -7,17 +7,17 @@ class AvailableActionPanel extends StatelessWidget { final List quickActions; final Listenable quickActionsChangeNotifier; final ValueNotifier panelHighlight; - final ValueNotifier draggedQuickAction; - final ValueNotifier draggedAvailableAction; - final bool Function(EntryAction action) removeQuickAction; + final ValueNotifier draggedQuickAction; + final ValueNotifier draggedAvailableAction; + final bool Function(EntryAction? action) removeQuickAction; const AvailableActionPanel({ - @required this.quickActions, - @required this.quickActionsChangeNotifier, - @required this.panelHighlight, - @required this.draggedQuickAction, - @required this.draggedAvailableAction, - @required this.removeQuickAction, + required this.quickActions, + required this.quickActionsChangeNotifier, + required this.panelHighlight, + required this.draggedQuickAction, + required this.draggedAvailableAction, + required this.removeQuickAction, }); static const allActions = [ @@ -53,7 +53,7 @@ class AvailableActionPanel extends StatelessWidget { return AnimatedBuilder( animation: Listenable.merge([quickActionsChangeNotifier, draggedAvailableAction]), builder: (context, child) => Padding( - padding: EdgeInsets.all(8), + padding: const EdgeInsets.all(8), child: Wrap( alignment: WrapAlignment.spaceEvenly, spacing: 8, @@ -95,9 +95,9 @@ class AvailableActionPanel extends StatelessWidget { child: child, ); - void _setDraggedQuickAction(EntryAction action) => draggedQuickAction.value = action; + void _setDraggedQuickAction(EntryAction? action) => draggedQuickAction.value = action; - void _setDraggedAvailableAction(EntryAction action) => draggedAvailableAction.value = action; + void _setDraggedAvailableAction(EntryAction? action) => draggedAvailableAction.value = action; void _setPanelHighlight(bool flag) => panelHighlight.value = flag; } diff --git a/lib/widgets/settings/quick_actions/common.dart b/lib/widgets/settings/quick_actions/common.dart index 7d98968ed..849091f5f 100644 --- a/lib/widgets/settings/quick_actions/common.dart +++ b/lib/widgets/settings/quick_actions/common.dart @@ -9,7 +9,7 @@ class ActionPanel extends StatelessWidget { const ActionPanel({ this.highlight = false, - @required this.child, + required this.child, }); @override @@ -18,16 +18,16 @@ class ActionPanel extends StatelessWidget { return AnimatedContainer( foregroundDecoration: BoxDecoration( color: color.withOpacity(.2), - border: Border.all( + border: Border.fromBorderSide(BorderSide( color: color, width: highlight ? 2 : 1, - ), - borderRadius: BorderRadius.circular(8), + )), + borderRadius: const BorderRadius.all(Radius.circular(8)), ), - margin: EdgeInsets.all(16), + margin: const EdgeInsets.all(16), duration: Durations.quickActionHighlightAnimation, child: ClipRRect( - borderRadius: BorderRadius.circular(8), + borderRadius: const BorderRadius.all(Radius.circular(8)), child: child, ), ); @@ -39,7 +39,7 @@ class ActionButton extends StatelessWidget { final bool enabled, showCaption; const ActionButton({ - @required this.action, + required this.action, this.enabled = true, this.showCaption = true, }); @@ -54,7 +54,7 @@ class ActionButton extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - SizedBox(height: padding), + const SizedBox(height: padding), OverlayButton( child: IconButton( icon: Icon(action.getIcon()), @@ -62,16 +62,16 @@ class ActionButton extends StatelessWidget { ), ), if (showCaption) ...[ - SizedBox(height: padding), + const SizedBox(height: padding), Text( action.getText(context), - style: enabled ? textStyle : textStyle.copyWith(color: textStyle.color.withOpacity(.2)), + style: enabled ? textStyle : textStyle!.copyWith(color: textStyle.color!.withOpacity(.2)), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2, ), ], - SizedBox(height: padding), + const SizedBox(height: padding), ], ), ); @@ -82,7 +82,7 @@ class DraggedPlaceholder extends StatelessWidget { final Widget child; const DraggedPlaceholder({ - @required this.child, + required this.child, }); @override diff --git a/lib/widgets/settings/quick_actions/editor.dart b/lib/widgets/settings/quick_actions/editor.dart index 9109dabd4..151c136ff 100644 --- a/lib/widgets/settings/quick_actions/editor.dart +++ b/lib/widgets/settings/quick_actions/editor.dart @@ -24,7 +24,7 @@ class QuickActionsTile extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - settings: RouteSettings(name: QuickActionEditorPage.routeName), + settings: const RouteSettings(name: QuickActionEditorPage.routeName), builder: (context) => QuickActionEditorPage(), ), ); @@ -42,10 +42,10 @@ class QuickActionEditorPage extends StatefulWidget { class _QuickActionEditorPageState extends State { final GlobalKey _animatedListKey = GlobalKey(debugLabel: 'quick-actions-animated-list'); - Timer _targetLeavingTimer; - List _quickActions; - final ValueNotifier _draggedQuickAction = ValueNotifier(null); - final ValueNotifier _draggedAvailableAction = ValueNotifier(null); + Timer? _targetLeavingTimer; + late List _quickActions; + final ValueNotifier _draggedQuickAction = ValueNotifier(null); + final ValueNotifier _draggedAvailableAction = ValueNotifier(null); final ValueNotifier _quickActionHighlight = ValueNotifier(false); final ValueNotifier _availableActionHighlight = ValueNotifier(false); final AChangeNotifier _quickActionsChangeNotifier = AChangeNotifier(); @@ -71,7 +71,7 @@ class _QuickActionEditorPageState extends State { void _onQuickActionTargetLeave() { _stopLeavingTimer(); final action = _draggedAvailableAction.value; - _targetLeavingTimer = Timer(Durations.quickActionListAnimation + Duration(milliseconds: 50), () { + _targetLeavingTimer = Timer(Durations.quickActionListAnimation + const Duration(milliseconds: 50), () { _removeQuickAction(action); _quickActionHighlight.value = false; }); @@ -111,18 +111,18 @@ class _QuickActionEditorPageState extends State { child: ListView( children: [ Padding( - padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Row( children: [ - Icon(AIcons.info), - SizedBox(width: 16), + const Icon(AIcons.info), + const SizedBox(width: 16), Expanded(child: Text(context.l10n.settingsViewerQuickActionEditorBanner)), ], ), ), - Divider(), + const Divider(), Padding( - padding: EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( context.l10n.settingsViewerQuickActionEditorDisplayedButtons, style: Constants.titleTextStyle, @@ -132,7 +132,7 @@ class _QuickActionEditorPageState extends State { valueListenable: _quickActionHighlight, builder: (context, highlight, child) => ActionPanel( highlight: highlight, - child: child, + child: child!, ), child: Container( height: OverlayButton.getSize(context) + quickActionVerticalPadding * 2, @@ -161,7 +161,7 @@ class _QuickActionEditorPageState extends State { shrinkWrap: true, padding: EdgeInsets.zero, itemBuilder: (context, index, animation) { - if (index >= _quickActions.length) return null; + if (index >= _quickActions.length) return const SizedBox(); final action = _quickActions[index]; return QuickActionButton( placement: QuickActionPlacement.action, @@ -186,14 +186,14 @@ class _QuickActionEditorPageState extends State { style: Theme.of(context).textTheme.caption, ), ) - : SizedBox(), + : const SizedBox(), ), ], ), ), ), Padding( - padding: EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( context.l10n.settingsViewerQuickActionEditorAvailableButtons, style: Constants.titleTextStyle, @@ -203,7 +203,7 @@ class _QuickActionEditorPageState extends State { valueListenable: _availableActionHighlight, builder: (context, highlight, child) => ActionPanel( highlight: highlight, - child: child, + child: child!, ), child: AvailableActionPanel( quickActions: _quickActions, @@ -224,14 +224,13 @@ class _QuickActionEditorPageState extends State { void _stopLeavingTimer() => _targetLeavingTimer?.cancel(); - bool _insertQuickAction(EntryAction action, QuickActionPlacement placement, EntryAction overAction) { - if (action == null) return false; + bool _insertQuickAction(EntryAction action, QuickActionPlacement placement, EntryAction? overAction) { _stopLeavingTimer(); if (_reordering) return false; final currentIndex = _quickActions.indexOf(action); final contained = currentIndex != -1; - int targetIndex; + int? targetIndex; switch (placement) { case QuickActionPlacement.header: targetIndex = 0; @@ -240,7 +239,7 @@ class _QuickActionEditorPageState extends State { targetIndex = _quickActions.length - (contained ? 1 : 0); break; case QuickActionPlacement.action: - targetIndex = _quickActions.indexOf(overAction); + targetIndex = _quickActions.indexOf(overAction!); break; } if (currentIndex == targetIndex) return false; @@ -248,7 +247,7 @@ class _QuickActionEditorPageState extends State { _reordering = true; _removeQuickAction(action); _quickActions.insert(targetIndex, action); - _animatedListKey.currentState.insertItem( + _animatedListKey.currentState!.insertItem( targetIndex, duration: Durations.quickActionListAnimation, ); @@ -257,12 +256,12 @@ class _QuickActionEditorPageState extends State { return true; } - bool _removeQuickAction(EntryAction action) { - if (!_quickActions.contains(action)) return false; + bool _removeQuickAction(EntryAction? action) { + if (action == null || !_quickActions.contains(action)) return false; final index = _quickActions.indexOf(action); _quickActions.removeAt(index); - _animatedListKey.currentState.removeItem( + _animatedListKey.currentState!.removeItem( index, (context, animation) => DraggedPlaceholder(child: _buildQuickActionButton(action, animation)), duration: Durations.quickActionListAnimation, @@ -279,7 +278,7 @@ class _QuickActionEditorPageState extends State { axis: Axis.horizontal, sizeFactor: animation, child: Padding( - padding: EdgeInsets.symmetric(vertical: _QuickActionEditorPageState.quickActionVerticalPadding, horizontal: 4), + padding: const EdgeInsets.symmetric(vertical: _QuickActionEditorPageState.quickActionVerticalPadding, horizontal: 4), child: OverlayButton( child: IconButton( icon: Icon(action.getIcon()), @@ -295,9 +294,9 @@ class _QuickActionEditorPageState extends State { builder: (context, child) { final dragged = _draggedQuickAction.value == action || _draggedAvailableAction.value == action; if (dragged) { - child = DraggedPlaceholder(child: child); + child = DraggedPlaceholder(child: child!); } - return child; + return child!; }, child: child, ); diff --git a/lib/widgets/settings/quick_actions/quick_actions.dart b/lib/widgets/settings/quick_actions/quick_actions.dart index c0c6aa2d7..9dd10212a 100644 --- a/lib/widgets/settings/quick_actions/quick_actions.dart +++ b/lib/widgets/settings/quick_actions/quick_actions.dart @@ -7,24 +7,24 @@ enum QuickActionPlacement { header, action, footer } class QuickActionButton extends StatelessWidget { final QuickActionPlacement placement; - final EntryAction action; + final EntryAction? action; final ValueNotifier panelHighlight; - final ValueNotifier draggedQuickAction; - final ValueNotifier draggedAvailableAction; - final bool Function(EntryAction action, QuickActionPlacement placement, EntryAction overAction) insertAction; + final ValueNotifier draggedQuickAction; + final ValueNotifier draggedAvailableAction; + final bool Function(EntryAction action, QuickActionPlacement placement, EntryAction? overAction) insertAction; final bool Function(EntryAction action) removeAction; final VoidCallback onTargetLeave; - final Widget child; + final Widget? child; const QuickActionButton({ - @required this.placement, + required this.placement, this.action, - @required this.panelHighlight, - @required this.draggedQuickAction, - @required this.draggedAvailableAction, - @required this.insertAction, - @required this.removeAction, - @required this.onTargetLeave, + required this.panelHighlight, + required this.draggedQuickAction, + required this.draggedAvailableAction, + required this.insertAction, + required this.removeAction, + required this.onTargetLeave, this.child, }); @@ -33,30 +33,30 @@ class QuickActionButton extends StatelessWidget { var child = this.child; child = _buildDragTarget(child); if (action != null) { - child = _buildDraggable(child); + child = _buildDraggable(child, action!); } return child; } - DragTarget _buildDragTarget(Widget child) { + DragTarget _buildDragTarget(Widget? child) { return DragTarget( onWillAccept: (data) { if (draggedQuickAction.value != null) { - insertAction(draggedQuickAction.value, placement, action); + insertAction(draggedQuickAction.value!, placement, action); } if (draggedAvailableAction.value != null) { - insertAction(draggedAvailableAction.value, placement, action); + insertAction(draggedAvailableAction.value!, placement, action); _setPanelHighlight(true); } return true; }, onAcceptWithDetails: (details) => _setPanelHighlight(false), onLeave: (data) => onTargetLeave(), - builder: (context, accepted, rejected) => child, + builder: (context, accepted, rejected) => child ?? const SizedBox(), ); } - Widget _buildDraggable(Widget child) => LongPressDraggable( + Widget _buildDraggable(Widget child, EntryAction action) => LongPressDraggable( data: action, maxSimultaneousDrags: 1, onDragStarted: () => _setDraggedQuickAction(action), @@ -74,7 +74,7 @@ class QuickActionButton extends StatelessWidget { child: child, ); - void _setDraggedQuickAction(EntryAction action) => draggedQuickAction.value = action; + void _setDraggedQuickAction(EntryAction? action) => draggedQuickAction.value = action; void _setPanelHighlight(bool flag) => panelHighlight.value = flag; } diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index d6d62725e..a8316fcf6 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -33,7 +33,7 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { - final ValueNotifier _expandedNotifier = ValueNotifier(null); + final ValueNotifier _expandedNotifier = ValueNotifier(null); @override Widget build(BuildContext context) { @@ -47,14 +47,14 @@ class _SettingsPageState extends State { data: theme.copyWith( textTheme: theme.textTheme.copyWith( // dense style font for tile subtitles, without modifying title font - bodyText2: TextStyle(fontSize: 12), + bodyText2: const TextStyle(fontSize: 12), ), ), child: SafeArea( child: Consumer( builder: (context, settings, child) => AnimationLimiter( child: ListView( - padding: EdgeInsets.all(8), + padding: const EdgeInsets.all(8), children: AnimationConfiguration.toStaggeredList( duration: Durations.staggeredAnimation, delay: Durations.staggeredAnimationDelay, @@ -133,7 +133,7 @@ class _SettingsPageState extends State { } Widget _buildThumbnailsSection(BuildContext context) { - final iconSize = IconTheme.of(context).size * MediaQuery.of(context).textScaleFactor; + final iconSize = IconTheme.of(context).size! * MediaQuery.of(context).textScaleFactor; double opacityFor(bool enabled) => enabled ? 1 : .2; return AvesExpansionTile( leading: _buildLeading(AIcons.grid, stringToColor('Thumbnails')), @@ -333,17 +333,17 @@ class _SettingsPageState extends State { } Widget _buildLeading(IconData icon, Color color) => Container( - padding: EdgeInsets.all(6), + padding: const EdgeInsets.all(6), decoration: BoxDecoration( - border: Border.all( + border: Border.fromBorderSide(BorderSide( color: color, width: AvesFilterChip.outlineWidth, - ), + )), shape: BoxShape.circle, ), child: DecoratedIcon( icon, - shadows: [Constants.embossShadow], + shadows: Constants.embossShadows, size: 18, ), ); diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart index 9cdfaac47..e5491e59a 100644 --- a/lib/widgets/stats/filter_table.dart +++ b/lib/widgets/stats/filter_table.dart @@ -14,10 +14,10 @@ class FilterTable extends StatelessWidget { final FilterCallback onFilterSelection; const FilterTable({ - @required this.totalEntryCount, - @required this.entryCountMap, - @required this.filterBuilder, - @required this.onFilterSelection, + required this.totalEntryCount, + required this.entryCountMap, + required this.filterBuilder, + required this.onFilterSelection, }); static const chipWidth = AvesFilterChip.maxChipWidth; @@ -36,7 +36,7 @@ class FilterTable extends StatelessWidget { final lineHeight = 16 * textScaleFactor; return Padding( - padding: EdgeInsetsDirectional.only(start: AvesFilterChip.outlineWidth / 2 + 6, end: 8), + padding: const EdgeInsetsDirectional.only(start: AvesFilterChip.outlineWidth / 2 + 6, end: 8), child: LayoutBuilder( builder: (context, constraints) { final showPercentIndicator = constraints.maxWidth - (chipWidth + countWidth) > percentIndicatorMinWidth; @@ -52,7 +52,7 @@ class FilterTable extends StatelessWidget { // the `Table` `border` property paints on the cells and does not add margins, // so we define margins here instead, but they should be symmetric // to keep all cells vertically aligned on the center/middle - margin: EdgeInsets.symmetric(vertical: 4), + margin: const EdgeInsets.symmetric(vertical: 4), alignment: AlignmentDirectional.centerStart, child: AvesFilterChip( filter: filter, @@ -69,20 +69,20 @@ class FilterTable extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: lineHeight), center: Text( NumberFormat.percentPattern().format(percent), - style: TextStyle(shadows: [Constants.embossShadow]), + style: const TextStyle(shadows: Constants.embossShadows), ), ), Text( '$count', - style: TextStyle(color: Colors.white70), + style: const TextStyle(color: Colors.white70), textAlign: TextAlign.end, ), ], ); }).toList(), columnWidths: { - 0: MaxColumnWidth(IntrinsicColumnWidth(), FixedColumnWidth(chipWidth)), - 2: MaxColumnWidth(IntrinsicColumnWidth(), FixedColumnWidth(countWidth)), + 0: const MaxColumnWidth(IntrinsicColumnWidth(), FixedColumnWidth(chipWidth)), + 2: const MaxColumnWidth(IntrinsicColumnWidth(), FixedColumnWidth(countWidth)), }, defaultVerticalAlignment: TableCellVerticalAlignment.middle, ); diff --git a/lib/widgets/stats/stats.dart b/lib/widgets/stats/stats.dart index a411a22f1..d84808cd9 100644 --- a/lib/widgets/stats/stats.dart +++ b/lib/widgets/stats/stats.dart @@ -16,6 +16,7 @@ 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'; import 'package:aves/widgets/stats/filter_table.dart'; +// ignore: import_of_legacy_library_into_null_safe import 'package:charts_flutter/flutter.dart' as charts; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -27,20 +28,20 @@ class StatsPage extends StatelessWidget { static const routeName = '/collection/stats'; final CollectionSource source; - final CollectionLens parentCollection; + final CollectionLens? parentCollection; final Map entryCountPerCountry = {}, entryCountPerPlace = {}, entryCountPerTag = {}; - Set get entries => parentCollection?.sortedEntries?.toSet() ?? source.visibleEntries; + Set get entries => parentCollection?.sortedEntries.toSet() ?? source.visibleEntries; static const mimeDonutMinWidth = 124.0; StatsPage({ - @required this.source, + required this.source, this.parentCollection, - }) : assert(source != null) { + }) { entries.forEach((entry) { if (entry.hasAddress) { - final address = entry.addressDetails; + final address = entry.addressDetails!; var country = address.countryName; if (country != null && country.isNotEmpty) { country += '${LocationFilter.locationSeparator}${address.countryCode}'; @@ -85,7 +86,7 @@ class StatsPage extends StatelessWidget { final textScaleFactor = MediaQuery.textScaleFactorOf(context); final lineHeight = 16 * textScaleFactor; final locationIndicator = Padding( - padding: EdgeInsets.all(16), + padding: const EdgeInsets.all(16), child: Column( children: [ LinearPercentIndicator( @@ -94,15 +95,15 @@ class StatsPage extends StatelessWidget { backgroundColor: Colors.white24, progressColor: Theme.of(context).accentColor, animation: true, - leading: Icon(AIcons.location), + leading: const Icon(AIcons.location), // right padding to match leading, so that inside label is aligned with outside label below - padding: EdgeInsets.symmetric(horizontal: lineHeight) + EdgeInsets.only(right: 24), + padding: EdgeInsets.symmetric(horizontal: lineHeight) + const EdgeInsets.only(right: 24), center: Text( NumberFormat.percentPattern().format(withGpsPercent), - style: TextStyle(shadows: [Constants.embossShadow]), + style: const TextStyle(shadows: Constants.embossShadows), ), ), - SizedBox(height: 8), + const SizedBox(height: 8), Text(context.l10n.statsWithGps(withGpsCount)), ], ), @@ -129,8 +130,8 @@ class StatsPage extends StatelessWidget { ); } - Widget _buildMimeDonut(BuildContext context, String Function(num) label, Map byMimeTypes) { - if (byMimeTypes.isEmpty) return SizedBox.shrink(); + Widget _buildMimeDonut(BuildContext context, String Function(int) label, Map byMimeTypes) { + if (byMimeTypes.isEmpty) return const SizedBox.shrink(); final sum = byMimeTypes.values.fold(0, (prev, v) => prev + v); @@ -191,12 +192,12 @@ class StatsPage extends StatelessWidget { WidgetSpan( alignment: PlaceholderAlignment.middle, child: Padding( - padding: EdgeInsetsDirectional.only(end: 8), + padding: const EdgeInsetsDirectional.only(end: 8), child: Icon(AIcons.disc, color: d.color), ), ), TextSpan(text: '${d.displayText} '), - TextSpan(text: '${d.entryCount}', style: TextStyle(color: Colors.white70)), + TextSpan(text: '${d.entryCount}', style: const TextStyle(color: Colors.white70)), ], ), overflow: TextOverflow.fade, @@ -233,7 +234,7 @@ class StatsPage extends StatelessWidget { return [ Padding( - padding: EdgeInsets.all(16), + padding: const EdgeInsets.all(16), child: Text( title, style: Constants.titleTextStyle, @@ -257,11 +258,11 @@ class StatsPage extends StatelessWidget { } void _applyToParentCollectionPage(BuildContext context, CollectionFilter filter) { - parentCollection.addFilter(filter); + parentCollection!.addFilter(filter); // 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); }); } @@ -270,7 +271,7 @@ class StatsPage extends StatelessWidget { Navigator.pushAndRemoveUntil( context, MaterialPageRoute( - settings: RouteSettings(name: CollectionPage.routeName), + settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage(CollectionLens( source: source, filters: [filter], @@ -287,8 +288,8 @@ class EntryByMimeDatum { final int entryCount; EntryByMimeDatum({ - @required this.mimeType, - @required this.entryCount, + required this.mimeType, + required this.entryCount, }) : displayText = MimeUtils.displayType(mimeType); Color get color => stringToColor(displayText); diff --git a/lib/widgets/viewer/debug/db.dart b/lib/widgets/viewer/debug/db.dart index e586c51d2..a6aa3601d 100644 --- a/lib/widgets/viewer/debug/db.dart +++ b/lib/widgets/viewer/debug/db.dart @@ -2,22 +2,23 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/services/services.dart'; import 'package:aves/widgets/viewer/info/common.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; class DbTab extends StatefulWidget { final AvesEntry entry; - const DbTab({@required this.entry}); + const DbTab({required this.entry}); @override _DbTabState createState() => _DbTabState(); } class _DbTabState extends State { - Future _dbDateLoader; - Future _dbEntryLoader; - Future _dbMetadataLoader; - Future _dbAddressLoader; + late Future _dbDateLoader; + late Future _dbEntryLoader; + late Future _dbMetadataLoader; + late Future _dbAddressLoader; AvesEntry get entry => widget.entry; @@ -29,23 +30,23 @@ class _DbTabState extends State { void _loadDatabase() { final contentId = entry.contentId; - _dbDateLoader = metadataDb.loadDates().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null)); - _dbEntryLoader = metadataDb.loadEntries().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null)); - _dbMetadataLoader = metadataDb.loadMetadataEntries().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null)); - _dbAddressLoader = metadataDb.loadAddresses().then((values) => values.firstWhere((row) => row.contentId == contentId, orElse: () => null)); + _dbDateLoader = metadataDb.loadDates().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId)); + _dbEntryLoader = metadataDb.loadEntries().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId)); + _dbMetadataLoader = metadataDb.loadMetadataEntries().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId)); + _dbAddressLoader = metadataDb.loadAddresses().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId)); setState(() {}); } @override Widget build(BuildContext context) { return ListView( - padding: EdgeInsets.all(16), + padding: const EdgeInsets.all(16), children: [ - FutureBuilder( + FutureBuilder( future: _dbDateLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); final data = snapshot.data; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -59,12 +60,12 @@ class _DbTabState extends State { ); }, ), - SizedBox(height: 16), - FutureBuilder( + const SizedBox(height: 16), + FutureBuilder( future: _dbEntryLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); final data = snapshot.data; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -88,12 +89,12 @@ class _DbTabState extends State { ); }, ), - SizedBox(height: 16), - FutureBuilder( + const SizedBox(height: 16), + FutureBuilder( future: _dbMetadataLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); final data = snapshot.data; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -115,12 +116,12 @@ class _DbTabState extends State { ); }, ), - SizedBox(height: 16), - FutureBuilder( + const SizedBox(height: 16), + FutureBuilder( future: _dbAddressLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); final data = snapshot.data; return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/widgets/viewer/debug_page.dart b/lib/widgets/viewer/debug/debug_page.dart similarity index 73% rename from lib/widgets/viewer/debug_page.dart rename to lib/widgets/viewer/debug/debug_page.dart index cd7300585..fb6e927a1 100644 --- a/lib/widgets/viewer/debug_page.dart +++ b/lib/widgets/viewer/debug/debug_page.dart @@ -16,21 +16,21 @@ class ViewerDebugPage extends StatelessWidget { final AvesEntry entry; - const ViewerDebugPage({@required this.entry}); + const ViewerDebugPage({required this.entry}); @override Widget build(BuildContext context) { final tabs = >[ - Tuple2(Tab(text: 'Entry'), _buildEntryTabView()), - if (context.select, bool>((vn) => vn.value != AppMode.view)) Tuple2(Tab(text: 'DB'), DbTab(entry: entry)), - Tuple2(Tab(icon: Icon(AIcons.android)), MetadataTab(entry: entry)), - Tuple2(Tab(icon: Icon(AIcons.image)), _buildThumbnailsTabView()), + Tuple2(const Tab(text: 'Entry'), _buildEntryTabView()), + if (context.select, bool>((vn) => vn.value != AppMode.view)) Tuple2(const Tab(text: 'DB'), DbTab(entry: entry)), + Tuple2(const Tab(icon: Icon(AIcons.android)), MetadataTab(entry: entry)), + Tuple2(const Tab(icon: Icon(AIcons.image)), _buildThumbnailsTabView()), ]; return DefaultTabController( length: tabs.length, child: Scaffold( appBar: AppBar( - title: Text('Debug'), + title: const Text('Debug'), bottom: TabBar( tabs: tabs.map((t) => t.item1).toList(), ), @@ -45,7 +45,7 @@ class ViewerDebugPage extends StatelessWidget { } Widget _buildEntryTabView() { - String toDateValue(int time, {int factor = 1}) { + String toDateValue(int? time, {int factor = 1}) { var value = '$time'; if (time != null && time > 0) { value += ' (${DateTime.fromMillisecondsSinceEpoch(time * factor)})'; @@ -54,7 +54,7 @@ class ViewerDebugPage extends StatelessWidget { } return ListView( - padding: EdgeInsets.all(16), + padding: const EdgeInsets.all(16), children: [ InfoRowGroup({ 'uri': '${entry.uri}', @@ -66,13 +66,13 @@ class ViewerDebugPage extends StatelessWidget { 'sourceMimeType': '${entry.sourceMimeType}', 'mimeType': '${entry.mimeType}', }), - Divider(), + const Divider(), InfoRowGroup({ 'dateModifiedSecs': toDateValue(entry.dateModifiedSecs, factor: 1000), 'sourceDateTakenMillis': toDateValue(entry.sourceDateTakenMillis), 'bestDate': '${entry.bestDate}', }), - Divider(), + const Divider(), InfoRowGroup({ 'width': '${entry.width}', 'height': '${entry.height}', @@ -83,12 +83,12 @@ class ViewerDebugPage extends StatelessWidget { 'displayAspectRatio': '${entry.displayAspectRatio}', 'displaySize': '${entry.displaySize}', }), - Divider(), + const Divider(), InfoRowGroup({ 'durationMillis': '${entry.durationMillis}', 'durationText': '${entry.durationText}', }), - Divider(), + const Divider(), InfoRowGroup({ 'sizeBytes': '${entry.sizeBytes}', 'isFavourite': '${entry.isFavourite}', @@ -104,7 +104,7 @@ class ViewerDebugPage extends StatelessWidget { 'canRotateAndFlip': '${entry.canRotateAndFlip}', 'xmpSubjects': '${entry.xmpSubjects}', }), - Divider(), + const Divider(), InfoRowGroup({ 'hasGps': '${entry.hasGps}', 'hasAddress': '${entry.hasAddress}', @@ -117,37 +117,36 @@ class ViewerDebugPage extends StatelessWidget { } Widget _buildThumbnailsTabView() { - const extent = 128.0; + final children = []; + if (entry.isSvg) { + const extent = 128.0; + children.addAll([ + const Text('SVG ($extent)'), + SvgPicture( + UriPicture( + uri: entry.uri, + mimeType: entry.mimeType, + ), + width: extent, + height: extent, + ) + ]); + } else { + children.addAll( + entry.cachedThumbnails.expand((provider) => [ + Text('Raster (${provider.key.extent})'), + Center( + child: Image( + image: provider, + ), + ), + const SizedBox(height: 16), + ]), + ); + } return ListView( - padding: EdgeInsets.all(16), - children: [ - if (entry.isSvg) ...[ - Text('SVG ($extent)'), - SvgPicture( - UriPicture( - uri: entry.uri, - mimeType: entry.mimeType, - ), - width: extent, - height: extent, - ) - ], - if (!entry.isSvg) ...[ - Text('Raster (fast)'), - Center( - child: Image( - image: entry.getThumbnail(), - ), - ), - SizedBox(height: 16), - Text('Raster ($extent)'), - Center( - child: Image( - image: entry.getThumbnail(extent: extent), - ), - ), - ], - ], + padding: const EdgeInsets.all(16), + children: children, ); } } diff --git a/lib/widgets/viewer/debug/metadata.dart b/lib/widgets/viewer/debug/metadata.dart index 63bb3b178..d0d55fd5b 100644 --- a/lib/widgets/viewer/debug/metadata.dart +++ b/lib/widgets/viewer/debug/metadata.dart @@ -12,14 +12,14 @@ import 'package:flutter/material.dart'; class MetadataTab extends StatefulWidget { final AvesEntry entry; - const MetadataTab({@required this.entry}); + const MetadataTab({required this.entry}); @override _MetadataTabState createState() => _MetadataTabState(); } class _MetadataTabState extends State { - Future _bitmapFactoryLoader, _contentResolverMetadataLoader, _exifInterfaceMetadataLoader, _mediaMetadataLoader, _metadataExtractorLoader, _tiffStructureLoader; + late Future _bitmapFactoryLoader, _contentResolverMetadataLoader, _exifInterfaceMetadataLoader, _mediaMetadataLoader, _metadataExtractorLoader, _tiffStructureLoader; // MediaStore timestamp keys static const secondTimestampKeys = ['date_added', 'date_modified', 'date_expires', 'isPlayed']; @@ -49,7 +49,7 @@ class _MetadataTabState extends State { final data = SplayTreeMap.of(snapshotData.map((k, v) { final key = k.toString(); var value = v?.toString() ?? 'null'; - if ([...secondTimestampKeys, ...millisecondTimestampKeys].contains(key) && v is num && v != 0) { + if ([...secondTimestampKeys, ...millisecondTimestampKeys].contains(key) && v is int && v != 0) { if (secondTimestampKeys.contains(key)) { v *= 1000; } @@ -62,28 +62,27 @@ class _MetadataTabState extends State { })); return AvesExpansionTile( title: title, - children: data.isNotEmpty - ? [ - Padding( - padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: InfoRowGroup( - data, - maxValueLength: Constants.infoGroupMaxValueLength, - ), - ) - ] - : null, + children: [ + if (data.isNotEmpty) + Padding( + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), + child: InfoRowGroup( + data, + maxValueLength: Constants.infoGroupMaxValueLength, + ), + ) + ], ); } Widget builderFromSnapshot(BuildContext context, AsyncSnapshot snapshot, String title) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); - return builderFromSnapshotData(context, snapshot.data, title); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + return builderFromSnapshotData(context, snapshot.data!, title); } return ListView( - padding: EdgeInsets.all(8), + padding: const EdgeInsets.all(8), children: [ FutureBuilder( future: _bitmapFactoryLoader, @@ -110,10 +109,10 @@ class _MetadataTabState extends State { future: _tiffStructureLoader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: snapshot.data.entries.map((kv) => builderFromSnapshotData(context, kv.value as Map, 'TIFF ${kv.key}')).toList(), + children: snapshot.data!.entries.map((kv) => builderFromSnapshotData(context, kv.value as Map, 'TIFF ${kv.key}')).toList(), ); }, ), diff --git a/lib/widgets/viewer/entry_action_delegate.dart b/lib/widgets/viewer/entry_action_delegate.dart index cf2ff7d53..a0c2da8c5 100644 --- a/lib/widgets/viewer/entry_action_delegate.dart +++ b/lib/widgets/viewer/entry_action_delegate.dart @@ -3,12 +3,16 @@ import 'dart:convert'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/highlight.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_op_events.dart'; import 'package:aves/services/services.dart'; +import 'package:aves/theme/durations.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'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; @@ -16,26 +20,25 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/rename_entry_dialog.dart'; import 'package:aves/widgets/filter_grids/album_pick.dart'; -import 'package:aves/widgets/viewer/debug_page.dart'; +import 'package:aves/widgets/viewer/debug/debug_page.dart'; import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:aves/widgets/viewer/printer.dart'; import 'package:aves/widgets/viewer/source_viewer_page.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:pedantic/pedantic.dart'; import 'package:provider/provider.dart'; class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { - final CollectionLens collection; + final CollectionLens? collection; final VoidCallback showInfo; EntryActionDelegate({ - @required this.collection, - @required this.showInfo, + required this.collection, + required this.showInfo, }); - bool get hasCollection => collection != null; - void onActionSelected(BuildContext context, AvesEntry entry, EntryAction action) { switch (action) { case EntryAction.toggleFavourite: @@ -76,7 +79,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix }); break; case EntryAction.openMap: - AndroidAppService.openMap(entry.geoUri).then((success) { + AndroidAppService.openMap(entry.geoUri!).then((success) { if (!success) showNoMatchingAppDialog(context); }); break; @@ -106,7 +109,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (!success) showFeedback(context, context.l10n.genericFailureFeedback); } - Future _rotate(BuildContext context, AvesEntry entry, {@required bool clockwise}) async { + Future _rotate(BuildContext context, AvesEntry entry, {required bool clockwise}) async { if (!await checkStoragePermission(context, {entry})) return; final success = await entry.rotate(clockwise: clockwise); @@ -140,8 +143,8 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (!await entry.delete()) { showFeedback(context, context.l10n.genericFailureFeedback); } else { - if (hasCollection) { - await collection.source.removeEntries({entry.uri}); + if (collection != null) { + await collection!.source.removeEntries({entry.uri}); } EntryDeletedNotification(entry).dispatch(context); } @@ -156,7 +159,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix final destinationAlbum = await Navigator.push( context, MaterialPageRoute( - settings: RouteSettings(name: AlbumPickPage.routeName), + settings: const RouteSettings(name: AlbumPickPage.routeName), builder: (context) => AlbumPickPage(source: source, moveType: MoveType.export), ), ); @@ -164,18 +167,18 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (destinationAlbum == null || destinationAlbum.isEmpty) return; if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return; - if (!await checkStoragePermission(context, {entry})) return; - if (!await checkFreeSpaceForMove(context, {entry}, destinationAlbum, MoveType.export)) return; final selection = {}; if (entry.isMultiPage) { final multiPageInfo = await metadataService.getMultiPageInfo(entry); - if (entry.isMotionPhoto) { - await multiPageInfo.extractMotionPhotoVideo(); - } - if (multiPageInfo.pageCount > 1) { - selection.addAll(multiPageInfo.exportEntries); + if (multiPageInfo != null) { + if (entry.isMotionPhoto) { + await multiPageInfo.extractMotionPhotoVideo(); + } + if (multiPageInfo.pageCount > 1) { + selection.addAll(multiPageInfo.exportEntries); + } } } else { selection.add(entry); @@ -193,11 +196,51 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix onDone: (processed) { final movedOps = processed.where((e) => e.success); final movedCount = movedOps.length; + final showAction = collection != null && movedCount > 0 + ? SnackBarAction( + label: context.l10n.showButtonLabel, + onPressed: () async { + final highlightInfo = context.read(); + final targetCollection = CollectionLens( + source: collection!.source, + filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))}, + groupFactor: collection!.groupFactor, + sortFactor: collection!.sortFactor, + ); + unawaited(Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + settings: const RouteSettings(name: CollectionPage.routeName), + builder: (context) { + return CollectionPage( + targetCollection, + ); + }, + ), + (route) => false, + )); + await Future.delayed(Durations.staggeredAnimationPageTarget + Durations.highlightScrollInitDelay); + final newUris = movedOps.map((v) => v.newFields['uri'] as String?).toSet(); + final targetEntry = targetCollection.sortedEntries.firstWhereOrNull((entry) => newUris.contains(entry.uri)); + if (targetEntry != null) { + highlightInfo.trackItem(targetEntry, highlightItem: targetEntry); + } + }, + ) + : null; if (movedCount < selectionCount) { final count = selectionCount - movedCount; - showFeedback(context, context.l10n.collectionExportFailureFeedback(count)); + showFeedback( + context, + context.l10n.collectionExportFailureFeedback(count), + showAction, + ); } else { - showFeedback(context, context.l10n.genericSuccessFeedback); + showFeedback( + context, + context.l10n.genericSuccessFeedback, + showAction, + ); } }, ); @@ -225,7 +268,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix Navigator.push( context, MaterialPageRoute( - settings: RouteSettings(name: SourceViewerPage.routeName), + settings: const RouteSettings(name: SourceViewerPage.routeName), builder: (context) => SourceViewerPage( loader: () => imageFileService.getSvg(entry.uri, entry.mimeType).then(utf8.decode), ), @@ -237,7 +280,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix Navigator.push( context, MaterialPageRoute( - settings: RouteSettings(name: ViewerDebugPage.routeName), + settings: const RouteSettings(name: ViewerDebugPage.routeName), builder: (context) => ViewerDebugPage(entry: entry), ), ); diff --git a/lib/widgets/viewer/entry_horizontal_pager.dart b/lib/widgets/viewer/entry_horizontal_pager.dart index 96dcd75cc..d2f09ccff 100644 --- a/lib/widgets/viewer/entry_horizontal_pager.dart +++ b/lib/widgets/viewer/entry_horizontal_pager.dart @@ -15,10 +15,10 @@ class MultiEntryScroller extends StatefulWidget { final void Function(String uri) onViewDisposed; const MultiEntryScroller({ - this.collection, - this.pageController, - this.onPageChanged, - this.onViewDisposed, + required this.collection, + required this.pageController, + required this.onPageChanged, + required this.onViewDisposed, }); @override @@ -35,23 +35,23 @@ class _MultiEntryScrollerState extends State with AutomaticK return MagnifierGestureDetectorScope( axis: [Axis.horizontal, Axis.vertical], child: PageView.builder( - key: Key('horizontal-pageview'), + key: const Key('horizontal-pageview'), scrollDirection: Axis.horizontal, controller: widget.pageController, - physics: MagnifierScrollerPhysics(parent: BouncingScrollPhysics()), + physics: const MagnifierScrollerPhysics(parent: BouncingScrollPhysics()), onPageChanged: widget.onPageChanged, itemBuilder: (context, index) { final entry = entries[index]; - Widget child; + Widget? child; if (entry.isMultiPage) { final multiPageController = context.read().getController(entry); if (multiPageController != null) { - child = StreamBuilder( + child = StreamBuilder( stream: multiPageController.infoStream, builder: (context, snapshot) { final multiPageInfo = multiPageController.info; - return ValueListenableBuilder( + return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { return _buildViewer(entry, pageEntry: multiPageInfo?.getPageEntryByIndex(page)); @@ -72,16 +72,16 @@ class _MultiEntryScrollerState extends State with AutomaticK ); } - Widget _buildViewer(AvesEntry mainEntry, {AvesEntry pageEntry}) { + Widget _buildViewer(AvesEntry mainEntry, {AvesEntry? pageEntry}) { return Selector( selector: (c, mq) => mq.size, builder: (c, mqSize, child) { return EntryPageView( - key: Key('imageview'), + key: const Key('imageview'), mainEntry: mainEntry, pageEntry: pageEntry ?? mainEntry, viewportSize: mqSize, - onDisposed: () => widget.onViewDisposed?.call(mainEntry.uri), + onDisposed: () => widget.onViewDisposed(mainEntry.uri), ); }, ); @@ -95,7 +95,7 @@ class SingleEntryScroller extends StatefulWidget { final AvesEntry entry; const SingleEntryScroller({ - this.entry, + required this.entry, }); @override @@ -109,15 +109,15 @@ class _SingleEntryScrollerState extends State with Automati Widget build(BuildContext context) { super.build(context); - Widget child; + Widget? child; if (mainEntry.isMultiPage) { final multiPageController = context.read().getController(mainEntry); if (multiPageController != null) { - child = StreamBuilder( + child = StreamBuilder( stream: multiPageController.infoStream, builder: (context, snapshot) { final multiPageInfo = multiPageController.info; - return ValueListenableBuilder( + return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { return _buildViewer(pageEntry: multiPageInfo?.getPageEntryByIndex(page)); @@ -135,7 +135,7 @@ class _SingleEntryScrollerState extends State with Automati ); } - Widget _buildViewer({AvesEntry pageEntry}) { + Widget _buildViewer({AvesEntry? pageEntry}) { return Selector( selector: (c, mq) => mq.size, builder: (c, mqSize, child) { diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index 1a4b00540..70fc8852e 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -1,7 +1,9 @@ +import 'dart:async'; import 'dart:math'; import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/theme/durations.dart'; 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'; @@ -11,22 +13,22 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; class ViewerVerticalPageView extends StatefulWidget { - final CollectionLens collection; - final ValueNotifier entryNotifier; + final CollectionLens? collection; + final ValueNotifier entryNotifier; final PageController horizontalPager, verticalPager; final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged; final VoidCallback onImagePageRequested; final void Function(String uri) onViewDisposed; const ViewerVerticalPageView({ - @required this.collection, - @required this.entryNotifier, - @required this.verticalPager, - @required this.horizontalPager, - @required this.onVerticalPageChanged, - @required this.onHorizontalPageChanged, - @required this.onImagePageRequested, - @required this.onViewDisposed, + required this.collection, + required this.entryNotifier, + required this.verticalPager, + required this.horizontalPager, + required this.onVerticalPageChanged, + required this.onHorizontalPageChanged, + required this.onImagePageRequested, + required this.onViewDisposed, }); @override @@ -35,14 +37,15 @@ class ViewerVerticalPageView extends StatefulWidget { class _ViewerVerticalPageViewState extends State { final ValueNotifier _backgroundColorNotifier = ValueNotifier(Colors.black); - final ValueNotifier _infoPageVisibleNotifier = ValueNotifier(false); - AvesEntry _oldEntry; + final ValueNotifier _isVerticallyScrollingNotifier = ValueNotifier(false); + Timer? _verticalScrollMonitoringTimer; + AvesEntry? _oldEntry; - CollectionLens get collection => widget.collection; + CollectionLens? get collection => widget.collection; bool get hasCollection => collection != null; - AvesEntry get entry => widget.entryNotifier.value; + AvesEntry? get entry => widget.entryNotifier.value; @override void initState() { @@ -60,6 +63,7 @@ class _ViewerVerticalPageViewState extends State { @override void dispose() { _unregisterWidget(widget); + _stopScrollMonitoringTimer(); super.dispose(); } @@ -72,35 +76,52 @@ class _ViewerVerticalPageViewState extends State { void _unregisterWidget(ViewerVerticalPageView widget) { widget.verticalPager.removeListener(_onVerticalPageControllerChanged); widget.entryNotifier.removeListener(_onEntryChanged); - _oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged); + _oldEntry?.imageChangeNotifier.removeListener(_onImageChanged); } @override Widget build(BuildContext context) { - final pages = [ - // fake page for opacity transition between collection and viewer - SizedBox(), - hasCollection - ? MultiEntryScroller( - collection: collection, - pageController: widget.horizontalPager, - onPageChanged: widget.onHorizontalPageChanged, - onViewDisposed: widget.onViewDisposed, - ) - : SingleEntryScroller( - entry: entry, - ), - NotificationListener( - onNotification: (notification) { - if (notification is BackUpNotification) widget.onImagePageRequested(); - return false; + // fake page for opacity transition between collection and viewer + const transitionPage = SizedBox(); + + final imagePage = hasCollection + ? MultiEntryScroller( + collection: collection!, + pageController: widget.horizontalPager, + onPageChanged: widget.onHorizontalPageChanged, + onViewDisposed: widget.onViewDisposed, + ) + : entry != null + ? SingleEntryScroller( + entry: entry!, + ) + : const SizedBox(); + + final infoPage = NotificationListener( + onNotification: (notification) { + widget.onImagePageRequested(); + return true; + }, + child: AnimatedBuilder( + animation: widget.verticalPager, + builder: (context, child) { + return Visibility( + visible: widget.verticalPager.page! > 1, + child: child!, + ); }, child: InfoPage( collection: collection, entryNotifier: widget.entryNotifier, - visibleNotifier: _infoPageVisibleNotifier, + isScrollingNotifier: _isVerticallyScrollingNotifier, ), ), + ); + + final pages = [ + transitionPage, + imagePage, + infoPage, ]; return ValueListenableBuilder( valueListenable: _backgroundColorNotifier, @@ -109,35 +130,42 @@ class _ViewerVerticalPageViewState extends State { child: child, ), child: PageView( - key: Key('vertical-pageview'), + key: const Key('vertical-pageview'), scrollDirection: Axis.vertical, controller: widget.verticalPager, - physics: MagnifierScrollerPhysics(parent: PageScrollPhysics()), - onPageChanged: (page) { - widget.onVerticalPageChanged(page); - _infoPageVisibleNotifier.value = page == pages.length - 1; - }, + physics: const MagnifierScrollerPhysics(parent: PageScrollPhysics()), + onPageChanged: widget.onVerticalPageChanged, children: pages, ), ); } void _onVerticalPageControllerChanged() { - final opacity = min(1.0, widget.verticalPager.page); + final opacity = min(1.0, widget.verticalPager.page!); _backgroundColorNotifier.value = _backgroundColorNotifier.value.withOpacity(opacity * opacity); + + _isVerticallyScrollingNotifier.value = true; + _stopScrollMonitoringTimer(); + _verticalScrollMonitoringTimer = Timer(Durations.infoScrollMonitoringTimerDelay, () { + _isVerticallyScrollingNotifier.value = false; + }); + } + + void _stopScrollMonitoringTimer() { + _verticalScrollMonitoringTimer?.cancel(); } // when the entry changed (e.g. by scrolling through the PageView, or if the entry got deleted) void _onEntryChanged() { - _oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged); + _oldEntry?.imageChangeNotifier.removeListener(_onImageChanged); _oldEntry = entry; if (entry != null) { - entry.imageChangeNotifier.addListener(_onImageChanged); + entry!.imageChangeNotifier.addListener(_onImageChanged); // make sure to locate the entry, // so that we can display the address instead of coordinates // even when initial collection locating has not reached this entry yet - entry.catalog(background: false).then((_) => entry.locate(background: false)); + entry!.catalog(background: false).then((_) => entry!.locate(background: false)); } else { Navigator.pop(context); } diff --git a/lib/widgets/viewer/entry_viewer_page.dart b/lib/widgets/viewer/entry_viewer_page.dart index 8fd6512aa..4c37a6012 100644 --- a/lib/widgets/viewer/entry_viewer_page.dart +++ b/lib/widgets/viewer/entry_viewer_page.dart @@ -10,13 +10,13 @@ import 'package:provider/provider.dart'; class EntryViewerPage extends StatelessWidget { static const routeName = '/viewer'; - final CollectionLens collection; + final CollectionLens? collection; final AvesEntry initialEntry; const EntryViewerPage({ - Key key, + Key? key, this.collection, - this.initialEntry, + required this.initialEntry, }) : super(key: key); @override diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 03aa7df4a..3a404382e 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/highlight.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/settings.dart'; @@ -26,6 +27,7 @@ import 'package:aves/widgets/viewer/overlay/top.dart'; import 'package:aves/widgets/viewer/video/conductor.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/visual/state.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -34,13 +36,13 @@ import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class EntryViewerStack extends StatefulWidget { - final CollectionLens collection; + final CollectionLens? collection; final AvesEntry initialEntry; const EntryViewerStack({ - Key key, + Key? key, this.collection, - @required this.initialEntry, + required this.initialEntry, }) : super(key: key); @override @@ -48,25 +50,26 @@ class EntryViewerStack extends StatefulWidget { } class _EntryViewerStackState extends State with SingleTickerProviderStateMixin, WidgetsBindingObserver { - final ValueNotifier _entryNotifier = ValueNotifier(null); - int _currentHorizontalPage; - ValueNotifier _currentVerticalPage; - PageController _horizontalPager, _verticalPager; + final ValueNotifier _entryNotifier = ValueNotifier(null); + late int _currentHorizontalPage; + late ValueNotifier _currentVerticalPage; + late PageController _horizontalPager, _verticalPager; final AChangeNotifier _verticalScrollNotifier = AChangeNotifier(); final ValueNotifier _overlayVisible = ValueNotifier(true); - AnimationController _overlayAnimationController; - Animation _topOverlayScale, _bottomOverlayScale; - Animation _bottomOverlayOffset; - EdgeInsets _frozenViewInsets, _frozenViewPadding; - EntryActionDelegate _actionDelegate; + late AnimationController _overlayAnimationController; + late Animation _topOverlayScale, _bottomOverlayScale; + late Animation _bottomOverlayOffset; + EdgeInsets? _frozenViewInsets, _frozenViewPadding; + late EntryActionDelegate _actionDelegate; final List>> _viewStateNotifiers = []; - final ValueNotifier _heroInfoNotifier = ValueNotifier(null); + final ValueNotifier _heroInfoNotifier = ValueNotifier(null); + bool _isEntryTracked = true; - CollectionLens get collection => widget.collection; + CollectionLens? get collection => widget.collection; bool get hasCollection => collection != null; - List get entries => hasCollection ? collection.sortedEntries : [widget.initialEntry]; + List get entries => hasCollection ? collection!.sortedEntries : [widget.initialEntry]; static const int transitionPage = 0; @@ -77,11 +80,12 @@ class _EntryViewerStackState extends State with SingleTickerPr @override void initState() { super.initState(); - final entry = widget.initialEntry; + // make sure initial entry is actually among the filtered collection entries + final entry = entries.contains(widget.initialEntry) ? widget.initialEntry : entries.firstOrNull; // opening hero, with viewer as target _heroInfoNotifier.value = HeroInfo(collection?.id, entry); _entryNotifier.value = entry; - _currentHorizontalPage = max(0, entries.indexOf(entry)); + _currentHorizontalPage = max(0, entry != null ? entries.indexOf(entry) : -1); _currentVerticalPage = ValueNotifier(imagePage); _horizontalPager = PageController(initialPage: _currentHorizontalPage); _verticalPager = PageController(initialPage: _currentVerticalPage.value)..addListener(_onVerticalPageControllerChange); @@ -99,7 +103,7 @@ class _EntryViewerStackState extends State with SingleTickerPr // no bounce at the bottom, to avoid video controller displacement curve: Curves.easeOutQuad, ); - _bottomOverlayOffset = Tween(begin: Offset(0, 1), end: Offset(0, 0)).animate(CurvedAnimation( + _bottomOverlayOffset = Tween(begin: const Offset(0, 1), end: const Offset(0, 0)).animate(CurvedAnimation( parent: _overlayAnimationController, curve: Curves.easeOutQuad, )); @@ -110,8 +114,8 @@ class _EntryViewerStackState extends State with SingleTickerPr ); _initEntryControllers(); _registerWidget(widget); - WidgetsBinding.instance.addObserver(this); - WidgetsBinding.instance.addPostFrameCallback((_) => _initOverlay()); + WidgetsBinding.instance!.addObserver(this); + WidgetsBinding.instance!.addPostFrameCallback((_) => _initOverlay()); if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { WindowService.keepScreenOn(true); } @@ -129,7 +133,7 @@ class _EntryViewerStackState extends State with SingleTickerPr _overlayAnimationController.dispose(); _overlayVisible.removeListener(_onOverlayVisibleChange); _verticalPager.removeListener(_onVerticalPageControllerChange); - WidgetsBinding.instance.removeObserver(this); + WidgetsBinding.instance!.removeObserver(this); _unregisterWidget(widget); super.dispose(); } @@ -164,14 +168,15 @@ class _EntryViewerStackState extends State with SingleTickerPr // back from info to image _goToVerticalPage(imagePage); } else { + if (!_isEntryTracked) _trackEntry(); _popVisual(); } return SynchronousFuture(false); }, - child: ValueListenableProvider.value( + child: ValueListenableProvider.value( value: _heroInfoNotifier, child: NotificationListener( - onNotification: (notification) { + onNotification: (dynamic notification) { if (notification is FilterSelectedNotification) { _goToCollection(notification.filter); } else if (notification is ViewStateNotification) { @@ -181,13 +186,10 @@ class _EntryViewerStackState extends State with SingleTickerPr } return false; }, - child: NotificationListener( + child: NotificationListener( onNotification: (notification) { - if (notification is ToggleOverlayNotification) { - _overlayVisible.value = !_overlayVisible.value; - return true; - } - return false; + _overlayVisible.value = !_overlayVisible.value; + return true; }, child: Stack( children: [ @@ -212,18 +214,17 @@ class _EntryViewerStackState extends State with SingleTickerPr ); } - void _updateViewState(String uri, ViewState viewState) { - final viewStateNotifier = _viewStateNotifiers.firstWhere((kv) => kv.item1 == uri, orElse: () => null)?.item2; + void _updateViewState(String uri, ViewState? viewState) { + final viewStateNotifier = _viewStateNotifiers.firstWhereOrNull((kv) => kv.item1 == uri)?.item2; viewStateNotifier?.value = viewState ?? ViewState.zero; } Widget _buildTopOverlay() { - final child = ValueListenableBuilder( + Widget child = ValueListenableBuilder( valueListenable: _entryNotifier, builder: (context, mainEntry, child) { - if (mainEntry == null) return SizedBox.shrink(); + if (mainEntry == null) return const SizedBox.shrink(); - final viewStateNotifier = _viewStateNotifiers.firstWhere((kv) => kv.item1 == mainEntry.uri, orElse: () => null)?.item2; return ViewerTopOverlay( mainEntry: mainEntry, scale: _topOverlayScale, @@ -244,32 +245,46 @@ class _EntryViewerStackState extends State with SingleTickerPr } _actionDelegate.onActionSelected(context, targetEntry, action); }, - viewStateNotifier: viewStateNotifier, + viewStateNotifier: _viewStateNotifiers.firstWhereOrNull((kv) => kv.item1 == mainEntry.uri)?.item2, ); }, ); - return ValueListenableBuilder( + + child = ValueListenableBuilder( valueListenable: _currentVerticalPage, builder: (context, page, child) { return Visibility( visible: page == imagePage, - child: child, + child: child!, ); }, child: child, ); + + child = ValueListenableBuilder( + valueListenable: _overlayAnimationController, + builder: (context, animation, child) { + return Visibility( + visible: !_overlayAnimationController.isDismissed, + child: child!, + ); + }, + child: child, + ); + + return child; } Widget _buildBottomOverlay() { - Widget bottomOverlay = ValueListenableBuilder( + Widget child = ValueListenableBuilder( valueListenable: _entryNotifier, - builder: (context, entry, child) { - if (entry == null) return SizedBox.shrink(); + builder: (context, mainEntry, child) { + if (mainEntry == null) return const SizedBox.shrink(); - Widget _buildExtraBottomOverlay(AvesEntry pageEntry) { + Widget? _buildExtraBottomOverlay(AvesEntry pageEntry) { // a 360 video is both a video and a panorama but only the video controls are displayed if (pageEntry.isVideo) { - return Selector( + return Selector( selector: (context, vc) => vc.getController(pageEntry), builder: (context, videoController, child) => VideoControlOverlay( entry: pageEntry, @@ -286,24 +301,24 @@ class _EntryViewerStackState extends State with SingleTickerPr return null; } - final multiPageController = entry.isMultiPage ? context.read().getController(entry) : null; + final multiPageController = mainEntry.isMultiPage ? context.read().getController(mainEntry) : null; final extraBottomOverlay = multiPageController != null - ? StreamBuilder( + ? StreamBuilder( stream: multiPageController.infoStream, builder: (context, snapshot) { final multiPageInfo = multiPageController.info; - if (multiPageInfo == null) return SizedBox.shrink(); - return ValueListenableBuilder( + if (multiPageInfo == null) return const SizedBox.shrink(); + return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { final pageEntry = multiPageInfo.getPageEntryByIndex(page); - return _buildExtraBottomOverlay(pageEntry) ?? SizedBox(); + return _buildExtraBottomOverlay(pageEntry) ?? const SizedBox(); }, ); }) - : _buildExtraBottomOverlay(entry); + : _buildExtraBottomOverlay(mainEntry); - final child = Column( + return Column( children: [ if (extraBottomOverlay != null) ExtraBottomOverlay( @@ -324,67 +339,75 @@ class _EntryViewerStackState extends State with SingleTickerPr ), ], ); - return ValueListenableBuilder( - valueListenable: _overlayAnimationController, - builder: (context, animation, child) { - return Visibility( - visible: _overlayAnimationController.status != AnimationStatus.dismissed, - child: child, - ); - }, - child: child, - ); }, ); - bottomOverlay = Selector( + child = Selector( selector: (c, mq) => mq.size.height, builder: (c, 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( bottom: (_verticalPager.position.hasPixels ? _verticalPager.offset : 0) - mqHeight, - child: child, + child: child!, ), child: child, ); }, - child: bottomOverlay, + child: child, + ); + + return ValueListenableBuilder( + valueListenable: _overlayAnimationController, + builder: (context, animation, child) { + return Visibility( + visible: !_overlayAnimationController.isDismissed, + child: child!, + ); + }, + child: child, ); - return bottomOverlay; } void _onVerticalPageControllerChange() { + if (!_isEntryTracked && _verticalPager.page?.floor() == transitionPage) { + _trackEntry(); + } _verticalScrollNotifier.notifyListeners(); } void _goToCollection(CollectionFilter filter) { + final baseCollection = collection; + if (baseCollection == null) return; _onLeave(); Navigator.pushAndRemoveUntil( context, MaterialPageRoute( - settings: RouteSettings(name: CollectionPage.routeName), - builder: (context) => CollectionPage( - CollectionLens( - source: collection.source, - filters: collection.filters, - groupFactor: collection.groupFactor, - sortFactor: collection.sortFactor, - )..addFilter(filter), - ), + settings: const RouteSettings(name: CollectionPage.routeName), + builder: (context) { + return CollectionPage( + CollectionLens( + source: baseCollection.source, + filters: baseCollection.filters, + groupFactor: baseCollection.groupFactor, + sortFactor: baseCollection.sortFactor, + )..addFilter(filter), + ); + }, ), (route) => false, ); } Future _goToVerticalPage(int page) { + // duration & curve should feel similar to changing page by vertical fling return _verticalPager.animateToPage( page, duration: Durations.viewerVerticalPageScrollAnimation, - curve: Curves.easeInOut, + curve: Curves.easeOutQuart, ); } @@ -410,7 +433,7 @@ class _EntryViewerStackState extends State with SingleTickerPr void _onEntryDeleted(BuildContext context, AvesEntry entry) { if (hasCollection) { - final entries = collection.sortedEntries; + final entries = collection!.sortedEntries; entries.remove(entry); if (entries.isEmpty) { Navigator.pop(context); @@ -424,16 +447,17 @@ class _EntryViewerStackState extends State with SingleTickerPr } Future _updateEntry() async { - if (_currentHorizontalPage != null && entries.isNotEmpty && _currentHorizontalPage >= entries.length) { + if (entries.isNotEmpty && _currentHorizontalPage >= entries.length) { // as of Flutter v1.22.2, `PageView` does not call `onPageChanged` when the last page is deleted // so we manually track the page change, and let the entry update follow _onHorizontalPageChanged(entries.length - 1); return; } - final newEntry = _currentHorizontalPage != null && _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null; + final newEntry = _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null; if (_entryNotifier.value == newEntry) return; _entryNotifier.value = newEntry; + _isEntryTracked = false; await _pauseVideoControllers(); await _initEntryControllers(); } @@ -450,7 +474,7 @@ class _EntryViewerStackState extends State with SingleTickerPr 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(); @@ -461,6 +485,20 @@ class _EntryViewerStackState extends State with SingleTickerPr } } + // track item when returning to collection, + // if they are not fully visible already + void _trackEntry() { + _isEntryTracked = true; + final entry = _entryNotifier.value; + if (entry != null && hasCollection) { + context.read().trackItem( + entry, + predicate: (v) => v < 1, + animate: false, + ); + } + } + void _onLeave() { _showSystemUI(); if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { @@ -479,7 +517,7 @@ class _EntryViewerStackState extends State with SingleTickerPr Future _initOverlay() async { // wait for MaterialPageRoute.transitionDuration // to show overlay after hero animation is complete - await Future.delayed(ModalRoute.of(context).transitionDuration * timeDilation); + await Future.delayed(ModalRoute.of(context)!.transitionDuration * timeDilation); await _onOverlayVisibleChange(); } @@ -527,7 +565,7 @@ class _EntryViewerStackState extends State with SingleTickerPr void _initViewStateController(AvesEntry entry) { final uri = entry.uri; - var controller = _viewStateNotifiers.firstWhere((kv) => kv.item1 == uri, orElse: () => null); + var controller = _viewStateNotifiers.firstWhereOrNull((kv) => kv.item1 == uri); if (controller != null) { _viewStateNotifiers.remove(controller); } else { @@ -553,6 +591,9 @@ class _EntryViewerStackState extends State with SingleTickerPr setState(() {}); final multiPageInfo = multiPageController.info ?? await multiPageController.infoStream.first; + assert(multiPageInfo != null); + if (multiPageInfo == null) return; + if (entry.isMotionPhoto) { await multiPageInfo.extractMotionPhotoVideo(); } @@ -568,11 +609,10 @@ class _EntryViewerStackState extends State with SingleTickerPr await _pauseVideoControllers(); if (settings.enableVideoAutoPlay) { final page = multiPageController.page; - final pageInfo = multiPageInfo.getByIndex(page); + final pageInfo = multiPageInfo.getByIndex(page)!; if (pageInfo.isVideo) { final pageEntry = multiPageInfo.getPageEntryByIndex(page); - final pageVideoController = videoConductor.getController(pageEntry); - assert(pageVideoController != null); + final pageVideoController = videoConductor.getController(pageEntry)!; await _playVideo(pageVideoController, () => entry == _entryNotifier.value && page == multiPageController.page); } } @@ -587,7 +627,7 @@ class _EntryViewerStackState extends State with SingleTickerPr // video decoding may fail or have initial artifacts when the player initializes // during this widget initialization (because of the page transition and hero animation?) // so we play after a delay for increased stability - await Future.delayed(Duration(milliseconds: 300) * timeDilation); + await Future.delayed(const Duration(milliseconds: 300) * timeDilation); await videoController.play(); diff --git a/lib/widgets/viewer/hero.dart b/lib/widgets/viewer/hero.dart index 80fa947fa..b502a0a93 100644 --- a/lib/widgets/viewer/hero.dart +++ b/lib/widgets/viewer/hero.dart @@ -5,8 +5,8 @@ class HeroInfo { // hero tag should include a collection identifier, so that it animates // between different views of the entry in the same collection (e.g. thumbnails <-> viewer) // but not between different collection instances, even with the same attributes (e.g. reloading collection page via drawer) - final int collectionId; - final AvesEntry entry; + final int? collectionId; + final AvesEntry? entry; const HeroInfo(this.collectionId, this.entry); diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index cae16dfa1..8de74ef42 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -14,26 +14,25 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class BasicSection extends StatelessWidget { final AvesEntry entry; - final CollectionLens collection; - final ValueNotifier visibleNotifier; + final CollectionLens? collection; final FilterCallback onFilter; const BasicSection({ - Key key, - @required this.entry, + Key? key, + required this.entry, this.collection, - @required this.visibleNotifier, - @required this.onFilter, + required this.onFilter, }) : super(key: key); int get megaPixels => entry.megaPixels; - bool get showMegaPixels => entry.isPhoto && megaPixels != null && megaPixels > 0; + bool get showMegaPixels => entry.isPhoto && megaPixels > 0; String get rasterResolutionText => '${entry.resolutionText}${showMegaPixels ? ' • $megaPixels MP' : ''}'; @@ -48,7 +47,7 @@ class BasicSection extends StatelessWidget { // 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 uri = entry.uri ?? infoUnknown; + final uri = entry.uri; final path = entry.path; return Column( @@ -59,13 +58,12 @@ class BasicSection extends StatelessWidget { l10n.viewerInfoLabelDate: dateText, if (entry.isVideo) ..._buildVideoRows(context), if (!entry.isSvg && entry.isSized) l10n.viewerInfoLabelResolution: rasterResolutionText, - l10n.viewerInfoLabelSize: entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : infoUnknown, + l10n.viewerInfoLabelSize: entry.sizeBytes != null ? formatFilesize(entry.sizeBytes!) : infoUnknown, l10n.viewerInfoLabelUri: uri, if (path != null) l10n.viewerInfoLabelPath: path, }), OwnerProp( entry: entry, - visibleNotifier: visibleNotifier, ), _buildChips(context), ], @@ -83,7 +81,7 @@ class BasicSection extends StatelessWidget { if (entry.isImage && entry.is360) TypeFilter.panorama, if (entry.isVideo && entry.is360) TypeFilter.sphericalVideo, if (entry.isVideo && !entry.is360) MimeFilter.video, - if (album != null) AlbumFilter(album, collection?.source?.getAlbumDisplayName(context, album)), + if (album != null) AlbumFilter(album, collection?.source.getAlbumDisplayName(context, album)), ...tags.map((tag) => TagFilter(tag)), }; return AnimatedBuilder( @@ -93,9 +91,9 @@ class BasicSection extends StatelessWidget { ...filters, if (entry.isFavourite) FavouriteFilter.instance, ]..sort(); - if (effectiveFilters.isEmpty) return SizedBox.shrink(); + if (effectiveFilters.isEmpty) return const SizedBox.shrink(); return Padding( - padding: EdgeInsets.symmetric(horizontal: AvesFilterChip.outlineWidth / 2) + EdgeInsets.only(top: 8), + padding: const EdgeInsets.symmetric(horizontal: AvesFilterChip.outlineWidth / 2) + const EdgeInsets.only(top: 8), child: Wrap( spacing: 8, runSpacing: 8, @@ -120,11 +118,9 @@ class BasicSection extends StatelessWidget { class OwnerProp extends StatefulWidget { final AvesEntry entry; - final ValueNotifier visibleNotifier; const OwnerProp({ - @required this.entry, - @required this.visibleNotifier, + required this.entry, }); @override @@ -132,53 +128,33 @@ class OwnerProp extends StatefulWidget { } class _OwnerPropState extends State { - final ValueNotifier _loadedUri = ValueNotifier(null); - String _ownerPackage; + late Future _ownerPackageFuture; AvesEntry get entry => widget.entry; - bool get isVisible => widget.visibleNotifier.value; - static const iconSize = 20.0; @override void initState() { super.initState(); - _registerWidget(widget); - _getOwner(); - } - - @override - void didUpdateWidget(covariant OwnerProp oldWidget) { - super.didUpdateWidget(oldWidget); - _unregisterWidget(oldWidget); - _registerWidget(widget); - _getOwner(); - } - - @override - void dispose() { - _unregisterWidget(widget); - super.dispose(); - } - - void _registerWidget(OwnerProp widget) { - widget.visibleNotifier.addListener(_getOwner); - } - - void _unregisterWidget(OwnerProp widget) { - widget.visibleNotifier.removeListener(_getOwner); + final isMediaContent = entry.uri.startsWith('content://media/external/'); + if (isMediaContent) { + _ownerPackageFuture = metadataService.getContentResolverProp(entry, 'owner_package_name'); + } else { + _ownerPackageFuture = SynchronousFuture(null); + } } @override Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: _loadedUri, - builder: (context, uri, child) { - if (_ownerPackage == null) return SizedBox(); - final appName = androidFileUtils.getCurrentAppName(_ownerPackage) ?? _ownerPackage; + return FutureBuilder( + future: _ownerPackageFuture, + builder: (context, snapshot) { + final ownerPackage = snapshot.data; + if (ownerPackage == null) return const SizedBox(); + final appName = androidFileUtils.getCurrentAppName(ownerPackage) ?? ownerPackage; // as of Flutter v1.22.6, `SelectableText` cannot contain `WidgetSpan` - // so be use a basic `Text` instead + // so we use a basic `Text` instead return Text.rich( TextSpan( children: [ @@ -188,14 +164,14 @@ class _OwnerPropState extends State { ), // `com.android.shell` is the package reported // for images copied to the device by ADB for Test Driver - if (_ownerPackage != 'com.android.shell') + if (ownerPackage != 'com.android.shell') WidgetSpan( alignment: PlaceholderAlignment.middle, child: Padding( - padding: EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.symmetric(horizontal: 4), child: Image( image: AppIconImage( - packageName: _ownerPackage, + packageName: ownerPackage, size: iconSize, ), width: iconSize, @@ -213,17 +189,4 @@ class _OwnerPropState extends State { }, ); } - - Future _getOwner() async { - if (entry == null) return; - if (_loadedUri.value == entry.uri) return; - final isMediaContent = entry.uri.startsWith('content://media/external/'); - if (isVisible && isMediaContent) { - _ownerPackage = await metadataService.getContentResolverProp(entry, 'owner_package_name'); - _loadedUri.value = entry.uri; - } else { - _ownerPackage = null; - _loadedUri.value = null; - } - } } diff --git a/lib/widgets/viewer/info/common.dart b/lib/widgets/viewer/info/common.dart index becd341bc..2472d9610 100644 --- a/lib/widgets/viewer/info/common.dart +++ b/lib/widgets/viewer/info/common.dart @@ -13,7 +13,7 @@ class SectionRow extends StatelessWidget { @override Widget build(BuildContext context) { const dim = 32.0; - Widget buildDivider() => SizedBox( + Widget buildDivider() => const SizedBox( width: dim, child: Divider( thickness: AvesFilterChip.outlineWidth, @@ -25,7 +25,7 @@ class SectionRow extends StatelessWidget { children: [ buildDivider(), Padding( - padding: EdgeInsets.all(16), + padding: const EdgeInsets.all(16), child: Icon( icon, size: dim, @@ -40,12 +40,12 @@ class SectionRow extends StatelessWidget { class InfoRowGroup extends StatefulWidget { final Map keyValues; final int maxValueLength; - final Map linkHandlers; + final Map? linkHandlers; static const keyValuePadding = 16; static const linkColor = Colors.blue; static const fontSize = 13.0; - static final baseStyle = TextStyle(fontSize: fontSize); + static const baseStyle = TextStyle(fontSize: fontSize); static final keyStyle = baseStyle.copyWith(color: Colors.white70, height: 2.0); static final linkStyle = baseStyle.copyWith(color: linkColor, decoration: TextDecoration.underline); @@ -66,11 +66,11 @@ class _InfoRowGroupState extends State { int get maxValueLength => widget.maxValueLength; - Map get linkHandlers => widget.linkHandlers; + Map? get linkHandlers => widget.linkHandlers; @override Widget build(BuildContext context) { - if (keyValues.isEmpty) return SizedBox.shrink(); + if (keyValues.isEmpty) return const SizedBox.shrink(); // compute the size of keys and space in order to align values final textScaleFactor = MediaQuery.textScaleFactorOf(context); @@ -93,11 +93,11 @@ class _InfoRowGroupState extends State { (kv) { final key = kv.key; String value; - TextStyle style; - GestureRecognizer recognizer; + TextStyle? style; + GestureRecognizer? recognizer; if (linkHandlers?.containsKey(key) == true) { - final handler = linkHandlers[key]; + final handler = linkHandlers![key]!; value = handler.linkText(context); // open link on tap recognizer = TapGestureRecognizer()..onTap = () => handler.onTap(context); @@ -119,7 +119,7 @@ class _InfoRowGroupState extends State { // as of Flutter v1.22.6, `SelectableText` cannot contain `WidgetSpan` // so we add padding using multiple hair spaces instead - final thisSpaceSize = max(0.0, (baseValueX - keySizes[key])) + InfoRowGroup.keyValuePadding; + final thisSpaceSize = max(0.0, (baseValueX - keySizes[key]!)) + InfoRowGroup.keyValuePadding; final spaceCount = (100 * thisSpaceSize / baseSpaceWidth).round(); return [ @@ -143,7 +143,7 @@ class _InfoRowGroupState extends State { span, textDirection: TextDirection.ltr, textScaleFactor: textScaleFactor, - )..layout(BoxConstraints(), parentUsesSize: true); + )..layout(const BoxConstraints(), parentUsesSize: true); return para.getMaxIntrinsicWidth(double.infinity); } } @@ -153,7 +153,7 @@ class InfoLinkHandler { final void Function(BuildContext context) onTap; const InfoLinkHandler({ - @required this.linkText, - @required this.onTap, + required this.linkText, + required this.onTap, }); } diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart index 7ee11eb9d..8e931288a 100644 --- a/lib/widgets/viewer/info/info_app_bar.dart +++ b/lib/widgets/viewer/info/info_app_bar.dart @@ -12,17 +12,17 @@ class InfoAppBar extends StatelessWidget { final VoidCallback onBackPressed; const InfoAppBar({ - @required this.entry, - @required this.metadataNotifier, - @required this.onBackPressed, + required this.entry, + required this.metadataNotifier, + required this.onBackPressed, }); @override Widget build(BuildContext context) { return SliverAppBar( leading: IconButton( - key: Key('back-button'), - icon: Icon(AIcons.goUp), + key: const Key('back-button'), + icon: const Icon(AIcons.goUp), onPressed: onBackPressed, tooltip: context.l10n.viewerInfoBackToViewerTooltip, ), @@ -32,7 +32,7 @@ class InfoAppBar extends StatelessWidget { ), actions: [ IconButton( - icon: Icon(AIcons.search), + icon: const Icon(AIcons.search), onPressed: () => _goToSearch(context), tooltip: MaterialLocalizations.of(context).searchFieldLabel, ), diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 741679d17..2bf631398 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -15,15 +15,15 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class InfoPage extends StatefulWidget { - final CollectionLens collection; - final ValueNotifier entryNotifier; - final ValueNotifier visibleNotifier; + final CollectionLens? collection; + final ValueNotifier entryNotifier; + final ValueNotifier isScrollingNotifier; const InfoPage({ - Key key, - @required this.collection, - @required this.entryNotifier, - @required this.visibleNotifier, + Key? key, + required this.collection, + required this.entryNotifier, + required this.isScrollingNotifier, }) : super(key: key); @override @@ -34,9 +34,9 @@ class _InfoPageState extends State { final ScrollController _scrollController = ScrollController(); bool _scrollStartFromTop = false; - CollectionLens get collection => widget.collection; + CollectionLens? get collection => widget.collection; - AvesEntry get entry => widget.entryNotifier.value; + AvesEntry? get entry => widget.entryNotifier.value; @override Widget build(BuildContext context) { @@ -45,7 +45,7 @@ class _InfoPageState extends State { body: GestureAreaProtectorStack( child: SafeArea( bottom: false, - child: NotificationListener( + child: NotificationListener( onNotification: _handleTopScroll, child: NotificationListener( onNotification: (notification) { @@ -55,19 +55,19 @@ class _InfoPageState extends State { child: Selector( selector: (c, mq) => mq.size.width, builder: (c, mqWidth, child) { - return ValueListenableBuilder( + return ValueListenableBuilder( valueListenable: widget.entryNotifier, builder: (context, entry, child) { return entry != null ? _InfoPageContent( collection: collection, entry: entry, - visibleNotifier: widget.visibleNotifier, + isScrollingNotifier: widget.isScrollingNotifier, scrollController: _scrollController, split: mqWidth > 600, goToViewer: _goToViewer, ) - : SizedBox.shrink(); + : const SizedBox.shrink(); }, ); }, @@ -81,22 +81,20 @@ class _InfoPageState extends State { ); } - bool _handleTopScroll(Notification notification) { - if (notification is ScrollNotification) { - if (notification is ScrollStartNotification) { - final metrics = notification.metrics; - _scrollStartFromTop = metrics.pixels == metrics.minScrollExtent; - } - if (_scrollStartFromTop) { - if (notification is ScrollUpdateNotification) { - _scrollStartFromTop = notification.scrollDelta < 0; - } else if (notification is ScrollEndNotification) { + bool _handleTopScroll(ScrollNotification notification) { + if (notification is ScrollStartNotification) { + final metrics = notification.metrics; + _scrollStartFromTop = metrics.pixels == metrics.minScrollExtent; + } + if (_scrollStartFromTop) { + if (notification is ScrollUpdateNotification) { + _scrollStartFromTop = notification.scrollDelta! < 0; + } else if (notification is ScrollEndNotification) { + _scrollStartFromTop = false; + } else if (notification is OverscrollNotification) { + if (notification.overscroll < 0) { + _goToViewer(); _scrollStartFromTop = false; - } else if (notification is OverscrollNotification) { - if (notification.overscroll < 0) { - _goToViewer(); - _scrollStartFromTop = false; - } } } } @@ -116,7 +114,7 @@ class _InfoPageState extends State { Navigator.push( context, TransparentMaterialPageRoute( - settings: RouteSettings(name: EntryViewerPage.routeName), + settings: const RouteSettings(name: EntryViewerPage.routeName), pageBuilder: (c, a, sa) => EntryViewerPage( initialEntry: tempEntry, ), @@ -126,21 +124,21 @@ class _InfoPageState extends State { } class _InfoPageContent extends StatefulWidget { - final CollectionLens collection; + final CollectionLens? collection; final AvesEntry entry; - final ValueNotifier visibleNotifier; + final ValueNotifier isScrollingNotifier; final ScrollController scrollController; final bool split; final VoidCallback goToViewer; const _InfoPageContent({ - Key key, - @required this.collection, - @required this.entry, - @required this.visibleNotifier, - @required this.scrollController, - @required this.split, - @required this.goToViewer, + Key? key, + required this.collection, + required this.entry, + required this.isScrollingNotifier, + required this.scrollController, + required this.split, + required this.goToViewer, }) : super(key: key); @override @@ -152,18 +150,15 @@ class _InfoPageContentState extends State<_InfoPageContent> { final ValueNotifier> _metadataNotifier = ValueNotifier({}); - CollectionLens get collection => widget.collection; + CollectionLens? get collection => widget.collection; AvesEntry get entry => widget.entry; - ValueNotifier get visibleNotifier => widget.visibleNotifier; - @override Widget build(BuildContext context) { final basicSection = BasicSection( entry: entry, collection: collection, - visibleNotifier: visibleNotifier, onFilter: _goToCollection, ); final locationAtTop = widget.split && entry.hasGps; @@ -171,7 +166,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { collection: collection, entry: entry, showTitle: !locationAtTop, - visibleNotifier: visibleNotifier, + isScrollingNotifier: widget.isScrollingNotifier, onFilter: _goToCollection, ); final basicAndLocationSliver = locationAtTop @@ -180,7 +175,7 @@ class _InfoPageContentState extends State<_InfoPageContent> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: basicSection), - SizedBox(width: 8), + const SizedBox(width: 8), Expanded(child: locationSection), ], ), @@ -196,7 +191,6 @@ class _InfoPageContentState extends State<_InfoPageContent> { final metadataSliver = MetadataSectionSliver( entry: entry, metadataNotifier: _metadataNotifier, - visibleNotifier: visibleNotifier, ); return CustomScrollView( @@ -208,11 +202,11 @@ class _InfoPageContentState extends State<_InfoPageContent> { onBackPressed: widget.goToViewer, ), SliverPadding( - padding: horizontalPadding + EdgeInsets.only(top: 8), + padding: horizontalPadding + const EdgeInsets.only(top: 8), sliver: basicAndLocationSliver, ), SliverPadding( - padding: horizontalPadding + EdgeInsets.only(bottom: 8), + padding: horizontalPadding + const EdgeInsets.only(bottom: 8), sliver: metadataSliver, ), BottomPaddingSliver(), diff --git a/lib/widgets/viewer/info/info_search.dart b/lib/widgets/viewer/info/info_search.dart index 83a96338c..6bc972e68 100644 --- a/lib/widgets/viewer/info/info_search.dart +++ b/lib/widgets/viewer/info/info_search.dart @@ -17,9 +17,9 @@ class InfoSearchDelegate extends SearchDelegate { Map get metadata => metadataNotifier.value; InfoSearchDelegate({ - @required String searchFieldLabel, - @required this.entry, - @required this.metadataNotifier, + required String searchFieldLabel, + required this.entry, + required this.metadataNotifier, }) : super( searchFieldLabel: searchFieldLabel, ); @@ -41,7 +41,7 @@ class InfoSearchDelegate extends SearchDelegate { return [ if (query.isNotEmpty) IconButton( - icon: Icon(AIcons.clear), + icon: const Icon(AIcons.clear), onPressed: () { query = ''; showSuggestions(context); @@ -78,7 +78,7 @@ class InfoSearchDelegate extends SearchDelegate { Widget buildResults(BuildContext context) { if (query.isEmpty) { showSuggestions(context); - return SizedBox(); + return const SizedBox(); } final queryParts = query.toUpperCase().split(' ')..removeWhere((s) => s.isEmpty); @@ -118,7 +118,7 @@ class InfoSearchDelegate extends SearchDelegate { return true; }, child: ListView.builder( - padding: EdgeInsets.all(8), + padding: const EdgeInsets.all(8), itemBuilder: (context, index) => tiles[index], itemCount: tiles.length, ), @@ -130,7 +130,7 @@ class InfoSearchDelegate extends SearchDelegate { Navigator.push( context, TransparentMaterialPageRoute( - settings: RouteSettings(name: EntryViewerPage.routeName), + settings: const RouteSettings(name: EntryViewerPage.routeName), pageBuilder: (c, a, sa) => EntryViewerPage( initialEntry: tempEntry, ), diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index e030e804b..8084580bd 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -1,6 +1,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/settings/coordinate_format.dart'; +import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -15,22 +16,23 @@ import 'package:aves/widgets/viewer/info/maps/google_map.dart'; import 'package:aves/widgets/viewer/info/maps/leaflet_map.dart'; import 'package:aves/widgets/viewer/info/maps/marker.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class LocationSection extends StatefulWidget { - final CollectionLens collection; + final CollectionLens? collection; final AvesEntry entry; final bool showTitle; - final ValueNotifier visibleNotifier; + final ValueNotifier isScrollingNotifier; final FilterCallback onFilter; const LocationSection({ - Key key, - @required this.collection, - @required this.entry, - @required this.showTitle, - @required this.visibleNotifier, - @required this.onFilter, + Key? key, + required this.collection, + required this.entry, + required this.showTitle, + required this.isScrollingNotifier, + required this.onFilter, }) : super(key: key); @override @@ -38,12 +40,16 @@ class LocationSection extends StatefulWidget { } class _LocationSectionState extends State with TickerProviderStateMixin { - String _loadedUri; + // as of google_maps_flutter v2.0.6, Google Maps initialization is blocking + // 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; static const extent = 48.0; static const pointerSize = Size(8.0, 6.0); - CollectionLens get collection => widget.collection; + CollectionLens? get collection => widget.collection; AvesEntry get entry => widget.entry; @@ -69,97 +75,114 @@ class _LocationSectionState extends State with TickerProviderSt void _registerWidget(LocationSection widget) { widget.entry.metadataChangeNotifier.addListener(_handleChange); widget.entry.addressChangeNotifier.addListener(_handleChange); - widget.visibleNotifier.addListener(_handleChange); } void _unregisterWidget(LocationSection widget) { widget.entry.metadataChangeNotifier.removeListener(_handleChange); widget.entry.addressChangeNotifier.removeListener(_handleChange); - widget.visibleNotifier.removeListener(_handleChange); } @override Widget build(BuildContext context) { - final showMap = (_loadedUri == entry.uri) || (entry.hasGps && widget.visibleNotifier.value); - if (showMap) { - _loadedUri = entry.uri; - final filters = []; - if (entry.hasAddress) { - final address = entry.addressDetails; - final country = address.countryName; - if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, '$country${LocationFilter.locationSeparator}${address.countryCode}')); - final place = address.place; - if (place != null && place.isNotEmpty) filters.add(LocationFilter(LocationLevel.place, place)); - } + if (!entry.hasGps) return const SizedBox(); + final latLng = entry.latLng!; + final geoUri = entry.geoUri!; - Widget buildMarker(BuildContext context) { - return ImageMarker( + final filters = []; + if (entry.hasAddress) { + final address = entry.addressDetails!; + final country = address.countryName; + if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, '$country${LocationFilter.locationSeparator}${address.countryCode}')); + final place = address.place; + if (place != null && place.isNotEmpty) filters.add(LocationFilter(LocationLevel.place, place)); + } + + Widget buildMarker(BuildContext context) => ImageMarker( entry: entry, extent: extent, pointerSize: pointerSize, ); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.showTitle) SectionRow(AIcons.location), - FutureBuilder( - future: availability.isConnected, - builder: (context, snapshot) { - if (snapshot.data != true) return SizedBox(); - return NotificationListener( - onNotification: (notification) { - if (notification is MapStyleChangedNotification) setState(() {}); - return false; - }, - child: AnimatedSize( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.showTitle) const SectionRow(AIcons.location), + FutureBuilder( + future: availability.isConnected, + builder: (context, snapshot) { + if (snapshot.data != true) return const SizedBox(); + return Selector( + selector: (context, s) => s.infoMapStyle, + builder: (context, mapStyle, child) { + final isGoogleMaps = mapStyle.isGoogleMaps; + return AnimatedSize( alignment: Alignment.topCenter, curve: Curves.easeInOutCubic, duration: Durations.mapStyleSwitchAnimation, vsync: this, - child: settings.infoMapStyle.isGoogleMaps - ? EntryGoogleMap( - // `LatLng` used by `google_maps_flutter` is not the one from `latlong` package - latLng: Tuple2(entry.latLng.latitude, entry.latLng.longitude), - geoUri: entry.geoUri, - initialZoom: settings.infoMapZoom, - markerId: entry.uri ?? entry.path, - markerBuilder: buildMarker, - ) - : EntryLeafletMap( - latLng: entry.latLng, - geoUri: entry.geoUri, - initialZoom: settings.infoMapZoom, - style: settings.infoMapStyle, - markerSize: Size(extent, extent + pointerSize.height), - markerBuilder: buildMarker, + child: ValueListenableBuilder( + valueListenable: widget.isScrollingNotifier, + builder: (context, scrolling, child) { + if (!scrolling && isGoogleMaps) { + _googleMapsLoaded = true; + } + return Visibility( + visible: !isGoogleMaps || _googleMapsLoaded, + replacement: Stack( + children: [ + const MapDecorator(), + MapButtonPanel( + geoUri: geoUri, + zoomBy: (_) {}, + ), + ], ), - ), - ); - }, - ), - if (entry.hasGps) _AddressInfoGroup(entry: entry), - if (filters.isNotEmpty) - Padding( - padding: EdgeInsets.symmetric(horizontal: AvesFilterChip.outlineWidth / 2) + EdgeInsets.only(top: 8), - child: Wrap( - spacing: 8, - runSpacing: 8, - children: filters - .map((filter) => AvesFilterChip( - filter: filter, - onTap: widget.onFilter, - )) - .toList(), - ), + child: child!, + ); + }, + child: isGoogleMaps + ? EntryGoogleMap( + // `LatLng` used by `google_maps_flutter` is not the one from `latlong` package + latLng: Tuple2(latLng.latitude, latLng.longitude), + geoUri: geoUri, + initialZoom: settings.infoMapZoom, + markerId: entry.uri, + markerBuilder: buildMarker, + ) + : EntryLeafletMap( + latLng: latLng, + geoUri: geoUri, + initialZoom: settings.infoMapZoom, + style: settings.infoMapStyle, + markerSize: Size( + extent + ImageMarker.outerBorderWidth * 2, + extent + ImageMarker.outerBorderWidth * 2 + pointerSize.height, + ), + markerBuilder: buildMarker, + ), + ), + ); + }, + ); + }, + ), + _AddressInfoGroup(entry: entry), + if (filters.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: AvesFilterChip.outlineWidth / 2) + const EdgeInsets.only(top: 8), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: filters + .map((filter) => AvesFilterChip( + filter: filter, + onTap: widget.onFilter, + )) + .toList(), ), - ], - ); - } else { - _loadedUri = null; - return SizedBox.shrink(); - } + ), + ], + ); } void _handleChange() => setState(() {}); @@ -168,14 +191,14 @@ class _LocationSectionState extends State with TickerProviderSt class _AddressInfoGroup extends StatefulWidget { final AvesEntry entry; - const _AddressInfoGroup({@required this.entry}); + const _AddressInfoGroup({required this.entry}); @override _AddressInfoGroupState createState() => _AddressInfoGroupState(); } class _AddressInfoGroupState extends State<_AddressInfoGroup> { - Future _addressLineLoader; + late Future _addressLineLoader; AvesEntry get entry => widget.entry; @@ -192,14 +215,14 @@ class _AddressInfoGroupState extends State<_AddressInfoGroup> { @override Widget build(BuildContext context) { - return FutureBuilder( + return FutureBuilder( future: _addressLineLoader, builder: (context, snapshot) { final fullAddress = !snapshot.hasError && snapshot.connectionState == ConnectionState.done ? snapshot.data : null; final address = fullAddress ?? entry.shortAddress; final l10n = context.l10n; return InfoRowGroup({ - l10n.viewerInfoLabelCoordinates: settings.coordinateFormat.format(entry.latLng), + l10n.viewerInfoLabelCoordinates: settings.coordinateFormat.format(entry.latLng!), if (address.isNotEmpty) l10n.viewerInfoLabelAddress: address, }); }, diff --git a/lib/widgets/viewer/info/maps/common.dart b/lib/widgets/viewer/info/maps/common.dart index efc307b0c..696e09ddd 100644 --- a/lib/widgets/viewer/info/maps/common.dart +++ b/lib/widgets/viewer/info/maps/common.dart @@ -15,11 +15,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; class MapDecorator extends StatelessWidget { - final Widget child; + final Widget? child; - static final BorderRadius mapBorderRadius = BorderRadius.circular(24); // to match button circles + static const mapBorderRadius = BorderRadius.all(Radius.circular(24)); // to match button circles + static const mapBackground = Color(0xFFDBD5D3); + static const mapLoadingGrid = Color(0xFFC4BEBB); - const MapDecorator({@required this.child}); + const MapDecorator({this.child}); @override Widget build(BuildContext context) { @@ -31,9 +33,22 @@ class MapDecorator extends StatelessWidget { child: ClipRRect( borderRadius: mapBorderRadius, child: Container( - color: Colors.white70, + color: mapBackground, height: 200, - child: child, + child: Stack( + children: [ + const GridPaper( + color: mapLoadingGrid, + interval: 10, + divisions: 1, + subdivisions: 1, + child: CustomPaint( + size: Size.infinite, + ), + ), + if (child != null) child!, + ], + ), ), ), ); @@ -47,8 +62,8 @@ class MapButtonPanel extends StatelessWidget { static const double padding = 4; const MapButtonPanel({ - @required this.geoUri, - @required this.zoomBy, + required this.geoUri, + required this.zoomBy, }); @override @@ -57,7 +72,7 @@ class MapButtonPanel extends StatelessWidget { child: Align( alignment: AlignmentDirectional.centerEnd, child: Padding( - padding: EdgeInsets.all(padding), + padding: const EdgeInsets.all(padding), child: TooltipTheme( data: TooltipTheme.of(context).copyWith( preferBelow: false, @@ -72,7 +87,7 @@ class MapButtonPanel extends StatelessWidget { }), tooltip: context.l10n.entryActionOpenMap, ), - SizedBox(height: padding), + const SizedBox(height: padding), MapOverlayButton( icon: AIcons.layers, onPressed: () async { @@ -94,18 +109,17 @@ class MapButtonPanel extends StatelessWidget { await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); if (style != null && style != settings.infoMapStyle) { settings.infoMapStyle = style; - MapStyleChangedNotification().dispatch(context); } }, tooltip: context.l10n.viewerInfoMapStyleTooltip, ), - Spacer(), + const Spacer(), MapOverlayButton( icon: AIcons.zoomIn, onPressed: () => zoomBy(1), tooltip: context.l10n.viewerInfoMapZoomInTooltip, ), - SizedBox(height: padding), + const SizedBox(height: padding), MapOverlayButton( icon: AIcons.zoomOut, onPressed: () => zoomBy(-1), @@ -126,9 +140,9 @@ class MapOverlayButton extends StatelessWidget { final VoidCallback onPressed; const MapOverlayButton({ - @required this.icon, - @required this.tooltip, - @required this.onPressed, + required this.icon, + required this.tooltip, + required this.onPressed, }); @override @@ -139,7 +153,7 @@ class MapOverlayButton extends StatelessWidget { color: kOverlayBackgroundColor, child: Ink( decoration: BoxDecoration( - border: AvesCircleBorder.build(context), + border: AvesBorder.border, shape: BoxShape.circle, ), child: IconButton( @@ -154,5 +168,3 @@ class MapOverlayButton extends StatelessWidget { ); } } - -class MapStyleChangedNotification extends Notification {} diff --git a/lib/widgets/viewer/info/maps/google_map.dart b/lib/widgets/viewer/info/maps/google_map.dart index c4ebfc55a..267f949ef 100644 --- a/lib/widgets/viewer/info/maps/google_map.dart +++ b/lib/widgets/viewer/info/maps/google_map.dart @@ -17,12 +17,12 @@ class EntryGoogleMap extends StatefulWidget { final WidgetBuilder markerBuilder; EntryGoogleMap({ - Key key, - Tuple2 latLng, - this.geoUri, - this.initialZoom, - this.markerId, - this.markerBuilder, + Key? key, + required Tuple2 latLng, + required this.geoUri, + required this.initialZoom, + required this.markerId, + required this.markerBuilder, }) : latLng = LatLng(latLng.item1, latLng.item2), super(key: key); @@ -30,9 +30,9 @@ class EntryGoogleMap extends StatefulWidget { State createState() => _EntryGoogleMapState(); } -class _EntryGoogleMapState extends State with AutomaticKeepAliveClientMixin { - GoogleMapController _controller; - Completer _markerLoaderCompleter; +class _EntryGoogleMapState extends State { + GoogleMapController? _controller; + late Completer _markerLoaderCompleter; @override void initState() { @@ -44,7 +44,7 @@ class _EntryGoogleMapState extends State with AutomaticKeepAlive void didUpdateWidget(covariant EntryGoogleMap oldWidget) { super.didUpdateWidget(oldWidget); if (widget.latLng != oldWidget.latLng && _controller != null) { - _controller.moveCamera(CameraUpdate.newLatLng(widget.latLng)); + _controller!.moveCamera(CameraUpdate.newLatLng(widget.latLng)); } if (widget.markerId != oldWidget.markerId) { _markerLoaderCompleter = Completer(); @@ -59,7 +59,6 @@ class _EntryGoogleMapState extends State with AutomaticKeepAlive @override Widget build(BuildContext context) { - super.build(context); return Stack( children: [ MarkerGeneratorWidget( @@ -84,7 +83,7 @@ class _EntryGoogleMapState extends State with AutomaticKeepAlive builder: (context, snapshot) { final markers = {}; if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) { - final markerBytes = snapshot.data; + final markerBytes = snapshot.data!; markers.add(Marker( markerId: MarkerId(widget.markerId), icon: BitmapDescriptor.fromBytes(markerBytes), @@ -117,7 +116,7 @@ class _EntryGoogleMapState extends State with AutomaticKeepAlive void _zoomBy(double amount) { settings.infoMapZoom += amount; - _controller.animateCamera(CameraUpdate.zoomBy(amount)); + _controller?.animateCamera(CameraUpdate.zoomBy(amount)); } MapType _toMapStyle(EntryMapStyle style) { @@ -132,7 +131,4 @@ class _EntryGoogleMapState extends State with AutomaticKeepAlive return MapType.none; } } - - @override - bool get wantKeepAlive => true; } diff --git a/lib/widgets/viewer/info/maps/leaflet_map.dart b/lib/widgets/viewer/info/maps/leaflet_map.dart index 7780b1475..8b47d6be5 100644 --- a/lib/widgets/viewer/info/maps/leaflet_map.dart +++ b/lib/widgets/viewer/info/maps/leaflet_map.dart @@ -20,33 +20,32 @@ class EntryLeafletMap extends StatefulWidget { final WidgetBuilder markerBuilder; const EntryLeafletMap({ - Key key, - this.latLng, - this.geoUri, - this.initialZoom, - this.style, - this.markerBuilder, - this.markerSize, + Key? key, + required this.latLng, + required this.geoUri, + required this.initialZoom, + required this.style, + required this.markerBuilder, + required this.markerSize, }) : super(key: key); @override State createState() => _EntryLeafletMapState(); } -class _EntryLeafletMapState extends State with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { +class _EntryLeafletMapState extends State with TickerProviderStateMixin { final MapController _mapController = MapController(); @override void didUpdateWidget(covariant EntryLeafletMap oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.latLng != oldWidget.latLng && _mapController != null) { + if (widget.latLng != oldWidget.latLng) { _mapController.move(widget.latLng, settings.infoMapZoom); } } @override Widget build(BuildContext context) { - super.build(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -105,7 +104,7 @@ class _EntryLeafletMapState extends State with AutomaticKeepAli case EntryMapStyle.stamenWatercolor: return StamenWatercolorLayer(); default: - return SizedBox.shrink(); + return const SizedBox.shrink(); } } @@ -117,22 +116,22 @@ class _EntryLeafletMapState extends State with AutomaticKeepAli case EntryMapStyle.stamenWatercolor: return _buildAttributionMarkdown(context.l10n.mapAttributionStamen); default: - return SizedBox.shrink(); + return const SizedBox.shrink(); } } Widget _buildAttributionMarkdown(String data) { return Padding( - padding: EdgeInsets.only(top: 4), + padding: const EdgeInsets.only(top: 4), child: MarkdownBody( data: data, selectable: true, styleSheet: MarkdownStyleSheet( a: TextStyle(color: Theme.of(context).accentColor), - p: TextStyle(color: Colors.white70, fontSize: InfoRowGroup.fontSize), + p: const TextStyle(color: Colors.white70, fontSize: InfoRowGroup.fontSize), ), onTapLink: (text, href, title) async { - if (await canLaunch(href)) { + if (href != null && await canLaunch(href)) { await launch(href); } }, @@ -141,8 +140,6 @@ class _EntryLeafletMapState extends State with AutomaticKeepAli } void _zoomBy(double amount) { - if (_mapController == null) return; - final endZoom = (settings.infoMapZoom + amount).clamp(1.0, 16.0); settings.infoMapZoom = endZoom; @@ -159,9 +156,6 @@ class _EntryLeafletMapState extends State with AutomaticKeepAli }); controller.forward(); } - - @override - bool get wantKeepAlive => true; } class OSMHotLayer extends StatelessWidget { diff --git a/lib/widgets/viewer/info/maps/marker.dart b/lib/widgets/viewer/info/maps/marker.dart index ef019964d..f055b4d15 100644 --- a/lib/widgets/viewer/info/maps/marker.dart +++ b/lib/widgets/viewer/info/maps/marker.dart @@ -17,11 +17,13 @@ class ImageMarker extends StatelessWidget { static const double outerBorderWidth = 1.5; static const double innerBorderWidth = 2; static const outerBorderColor = Colors.white30; - static final innerBorderColor = Colors.grey[900]; + static const innerBorderColor = Color(0xFF212121); + static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim)); + static const innerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim - outerBorderWidth)); const ImageMarker({ - @required this.entry, - @required this.extent, + required this.entry, + required this.extent, this.pointerSize = Size.zero, }); @@ -37,8 +39,21 @@ class ImageMarker extends StatelessWidget { extent: extent, ); - final outerBorderRadius = BorderRadius.circular(outerBorderRadiusDim); - final innerBorderRadius = BorderRadius.circular(outerBorderRadiusDim - outerBorderWidth); + const outerDecoration = BoxDecoration( + border: Border.fromBorderSide(BorderSide( + color: outerBorderColor, + width: outerBorderWidth, + )), + borderRadius: outerBorderRadius, + ); + + const innerDecoration = BoxDecoration( + border: Border.fromBorderSide(BorderSide( + color: innerBorderColor, + width: innerBorderWidth, + )), + borderRadius: innerBorderRadius, + ); return CustomPaint( foregroundPainter: MarkerPointerPainter( @@ -50,21 +65,9 @@ class ImageMarker extends StatelessWidget { child: Padding( padding: EdgeInsets.only(bottom: pointerSize.height), child: Container( - decoration: BoxDecoration( - border: Border.all( - color: outerBorderColor, - width: outerBorderWidth, - ), - borderRadius: outerBorderRadius, - ), + decoration: outerDecoration, child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: innerBorderColor, - width: innerBorderWidth, - ), - borderRadius: innerBorderRadius, - ), + decoration: innerDecoration, position: DecorationPosition.foreground, child: ClipRRect( borderRadius: innerBorderRadius, @@ -83,10 +86,10 @@ class MarkerPointerPainter extends CustomPainter { final Size size; const MarkerPointerPainter({ - this.color, - this.outlineColor, - this.outlineWidth, - this.size, + required this.color, + required this.outlineColor, + required this.outlineWidth, + required this.size, }); @override @@ -127,10 +130,10 @@ class MarkerGeneratorWidget extends StatefulWidget { final Function(List bitmaps) onComplete; const MarkerGeneratorWidget({ - Key key, - @required this.markers, + Key? key, + required this.markers, this.delay = Duration.zero, - @required this.onComplete, + required this.onComplete, }) : super(key: key); @override @@ -143,7 +146,7 @@ class _MarkerGeneratorWidgetState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) async { + WidgetsBinding.instance!.addPostFrameCallback((_) async { if (widget.delay > Duration.zero) { await Future.delayed(widget.delay); } @@ -174,10 +177,10 @@ class _MarkerGeneratorWidgetState extends State { Future> _getBitmaps(BuildContext context) async { final pixelRatio = context.read().devicePixelRatio; return Future.wait(_globalKeys.map((key) async { - RenderRepaintBoundary boundary = key.currentContext.findRenderObject(); + final boundary = key.currentContext!.findRenderObject() as RenderRepaintBoundary; final image = await boundary.toImage(pixelRatio: pixelRatio); final byteData = await image.toByteData(format: ui.ImageByteFormat.png); - return byteData.buffer.asUint8List(); + return byteData != null ? byteData.buffer.asUint8List() : Uint8List(0); })); } } diff --git a/lib/widgets/viewer/info/maps/scale_layer.dart b/lib/widgets/viewer/info/maps/scale_layer.dart index 0a216c00e..e8805975a 100644 --- a/lib/widgets/viewer/info/maps/scale_layer.dart +++ b/lib/widgets/viewer/info/maps/scale_layer.dart @@ -11,7 +11,7 @@ class ScaleLayerOptions extends LayerOptions { final Widget Function(double width, String distance) builder; ScaleLayerOptions({ - Key key, + Key? key, this.builder = defaultBuilder, rebuild, }) : super(key: key, rebuild: rebuild); @@ -27,12 +27,12 @@ class ScaleLayerOptions extends LayerOptions { class ScaleLayerWidget extends StatelessWidget { final ScaleLayerOptions options; - ScaleLayerWidget({@required this.options}) : super(key: options.key); + ScaleLayerWidget({required this.options}) : super(key: options.key); @override Widget build(BuildContext context) { final mapState = MapState.maybeOf(context); - return mapState != null ? ScaleLayer(options, mapState, mapState.onMoved) : SizedBox(); + return mapState != null ? ScaleLayer(options, mapState, mapState.onMoved) : const SizedBox(); } } @@ -86,7 +86,7 @@ class ScaleLayer extends StatelessWidget { final targetPoint = util.calculateEndingGlobalCoordinates(center, 90, distance); final end = map.project(targetPoint); final displayDistance = distance > 999 ? '${(distance / 1000).toStringAsFixed(0)} km' : '${distance.toStringAsFixed(0)} m'; - final double width = (end.x - start.x); + final width = end.x - (start.x as double); return scaleLayerOpts.builder(width, displayDistance); }, @@ -104,22 +104,22 @@ class ScaleBar extends StatelessWidget { static const double barThickness = 1; const ScaleBar({ - @required this.distance, - @required this.width, + required this.distance, + required this.width, }); @override Widget build(BuildContext context) { return Container( alignment: AlignmentDirectional.bottomStart, - padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ OutlinedText( text: distance, - style: TextStyle( + style: const TextStyle( color: fillColor, fontSize: 11, ), @@ -129,13 +129,13 @@ class ScaleBar extends StatelessWidget { Container( height: barThickness + outlineWidth * 2, width: width, - decoration: BoxDecoration( + decoration: const BoxDecoration( color: fillColor, - border: Border.all( + border: Border.fromBorderSide(BorderSide( color: outlineColor, width: outlineWidth, - ), - borderRadius: BorderRadius.circular(8), + )), + borderRadius: BorderRadius.all(Radius.circular(8)), ), ), ], diff --git a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart index 078a39143..1e9aff201 100644 --- a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart +++ b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart @@ -26,13 +26,13 @@ class MetadataDirTile extends StatelessWidget with FeedbackMixin { final AvesEntry entry; final String title; final MetadataDirectory dir; - final ValueNotifier expandedDirectoryNotifier; + final ValueNotifier? expandedDirectoryNotifier; final bool initiallyExpanded, showThumbnails; const MetadataDirTile({ - @required this.entry, - @required this.title, - @required this.dir, + required this.entry, + required this.title, + required this.dir, this.expandedDirectoryNotifier, this.initiallyExpanded = false, this.showThumbnails = true, @@ -41,7 +41,7 @@ class MetadataDirTile extends StatelessWidget with FeedbackMixin { @override Widget build(BuildContext context) { final tags = dir.tags; - if (tags.isEmpty) return SizedBox.shrink(); + if (tags.isEmpty) return const SizedBox.shrink(); final dirName = dir.name; Widget tile; @@ -53,7 +53,7 @@ class MetadataDirTile extends StatelessWidget with FeedbackMixin { initiallyExpanded: initiallyExpanded, ); } else { - Map linkHandlers; + Map? linkHandlers; switch (dirName) { case SvgMetadataService.metadataDirectory: linkHandlers = getSvgLinkHandlers(tags); @@ -71,7 +71,7 @@ class MetadataDirTile extends StatelessWidget with FeedbackMixin { children: [ if (showThumbnails && dirName == MetadataDirectory.exifThumbnailDirectory) MetadataThumbnails(entry: entry), Padding( - padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), child: InfoRowGroup( tags, maxValueLength: Constants.infoGroupMaxValueLength, @@ -98,9 +98,9 @@ class MetadataDirTile extends StatelessWidget with FeedbackMixin { Navigator.push( context, MaterialPageRoute( - settings: RouteSettings(name: SourceViewerPage.routeName), + settings: const RouteSettings(name: SourceViewerPage.routeName), builder: (context) => SourceViewerPage( - loader: () => SynchronousFuture(tags['Metadata']), + loader: () => SynchronousFuture(tags['Metadata'] ?? ''), ), ), ); @@ -119,7 +119,7 @@ class MetadataDirTile extends StatelessWidget with FeedbackMixin { } Future _openEmbeddedData(BuildContext context, OpenEmbeddedDataNotification notification) async { - Map fields; + late Map fields; switch (notification.source) { case EmbeddedDataSource.motionPhotoVideo: fields = await embeddedDataService.extractMotionPhotoVideo(entry); @@ -131,13 +131,13 @@ class MetadataDirTile extends StatelessWidget with FeedbackMixin { fields = await embeddedDataService.extractXmpDataProp(entry, notification.propPath, notification.mimeType); break; } - if (fields == null || !fields.containsKey('mimeType') || !fields.containsKey('uri')) { + if (!fields.containsKey('mimeType') || !fields.containsKey('uri')) { showFeedback(context, context.l10n.viewerInfoOpenEmbeddedFailureFeedback); return; } - final mimeType = fields['mimeType']; - final uri = fields['uri']; + final mimeType = fields['mimeType']!; + final uri = fields['uri']!; if (!MimeTypes.isImage(mimeType) && !MimeTypes.isVideo(mimeType)) { // open with another app unawaited(AndroidAppService.open(uri, mimeType).then((success) { diff --git a/lib/widgets/viewer/info/metadata/metadata_section.dart b/lib/widgets/viewer/info/metadata/metadata_section.dart index 307a05343..c4134dd5e 100644 --- a/lib/widgets/viewer/info/metadata/metadata_section.dart +++ b/lib/widgets/viewer/info/metadata/metadata_section.dart @@ -19,31 +19,24 @@ import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; class MetadataSectionSliver extends StatefulWidget { final AvesEntry entry; - final ValueNotifier visibleNotifier; final ValueNotifier> metadataNotifier; const MetadataSectionSliver({ - @required this.entry, - @required this.visibleNotifier, - @required this.metadataNotifier, + required this.entry, + required this.metadataNotifier, }); @override State createState() => _MetadataSectionSliverState(); } -class _MetadataSectionSliverState extends State with AutomaticKeepAliveClientMixin { - final ValueNotifier _loadedMetadataUri = ValueNotifier(null); - final ValueNotifier _expandedDirectoryNotifier = ValueNotifier(null); +class _MetadataSectionSliverState extends State { + final ValueNotifier _expandedDirectoryNotifier = ValueNotifier(null); AvesEntry get entry => widget.entry; - bool get isVisible => widget.visibleNotifier.value; - ValueNotifier> get metadataNotifier => widget.metadataNotifier; - Map get metadata => metadataNotifier.value; - // directory names may contain the name of their parent directory // if so, they are separated by this character static const parentChildSeparator = '/'; @@ -71,18 +64,15 @@ class _MetadataSectionSliverState extends State with Auto } void _registerWidget(MetadataSectionSliver widget) { - widget.visibleNotifier.addListener(_getMetadata); widget.entry.metadataChangeNotifier.addListener(_onMetadataChanged); } void _unregisterWidget(MetadataSectionSliver widget) { - widget.visibleNotifier.removeListener(_getMetadata); widget.entry.metadataChangeNotifier.removeListener(_onMetadataChanged); } @override Widget build(BuildContext context) { - super.build(context); // use a `Column` inside a `SliverToBoxAdapter`, instead of a `SliverList`, // so that we can have the metadata-dependent `AnimationLimiter` inside the metadata section // warning: placing the `AnimationLimiter` as a parent to the `ScrollView` @@ -92,90 +82,81 @@ class _MetadataSectionSliverState extends State with Auto // cancel notification bubbling so that the info page // does not misinterpret content scrolling for page scrolling onNotification: (notification) => true, - child: ValueListenableBuilder( - valueListenable: _loadedMetadataUri, - builder: (context, uri, child) { - Widget content; - if (metadata.isEmpty) { - content = SizedBox.shrink(); - } else { - content = Column( - children: AnimationConfiguration.toStaggeredList( - duration: Durations.staggeredAnimation, - delay: Durations.staggeredAnimationDelay, - childAnimationBuilder: (child) => SlideAnimation( - verticalOffset: 50.0, - child: FadeInAnimation( - child: child, + child: ValueListenableBuilder>( + valueListenable: metadataNotifier, + builder: (context, metadata, child) { + Widget content; + if (metadata.isEmpty) { + content = const SizedBox.shrink(); + } else { + content = Column( + children: AnimationConfiguration.toStaggeredList( + duration: Durations.staggeredAnimation, + delay: Durations.staggeredAnimationDelay, + childAnimationBuilder: (child) => SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: child, + ), ), + children: [ + const SectionRow(AIcons.info), + ...metadata.entries.map((kv) => MetadataDirTile( + entry: entry, + title: kv.key, + dir: kv.value, + expandedDirectoryNotifier: _expandedDirectoryNotifier, + )), + ], ), - children: [ - SectionRow(AIcons.info), - ...metadata.entries.map((kv) => MetadataDirTile( - entry: entry, - title: kv.key, - dir: kv.value, - expandedDirectoryNotifier: _expandedDirectoryNotifier, - )), - ], - ), + ); + } + + return AnimationLimiter( + // we update the limiter key after fetching the metadata of a new entry, + // in order to restart the staggered animation of the metadata section + key: ValueKey(metadata.length), + child: content, ); - } - return AnimationLimiter( - // we update the limiter key after fetching the metadata of a new entry, - // in order to restart the staggered animation of the metadata section - key: Key(uri), - child: content, - ); - }, - ), + }), ), ); } void _onMetadataChanged() { - _loadedMetadataUri.value = null; metadataNotifier.value = {}; _getMetadata(); } Future _getMetadata() async { - if (entry == null) return; - if (_loadedMetadataUri.value == entry.uri) return; - if (isVisible) { - final rawMetadata = await (entry.isSvg ? SvgMetadataService.getAllMetadata(entry) : metadataService.getAllMetadata(entry)) ?? {}; - final directories = rawMetadata.entries.map((dirKV) { - var directoryName = dirKV.key as String ?? ''; + final rawMetadata = await (entry.isSvg ? SvgMetadataService.getAllMetadata(entry) : metadataService.getAllMetadata(entry)); + final directories = rawMetadata.entries.map((dirKV) { + var directoryName = dirKV.key as String; - String parent; - final parts = directoryName.split(parentChildSeparator); - if (parts.length > 1) { - parent = parts[0]; - directoryName = parts[1]; - } - - final rawTags = dirKV.value as Map ?? {}; - return MetadataDirectory(directoryName, parent, _toSortedTags(rawTags)); - }).toList(); - - if (entry.isVideo || (entry.mimeType == MimeTypes.heif && entry.isMultiPage)) { - directories.addAll(await _getStreamDirectories()); + String? parent; + final parts = directoryName.split(parentChildSeparator); + if (parts.length > 1) { + parent = parts[0]; + directoryName = parts[1]; } - final titledDirectories = directories.map((dir) { - var title = dir.name; - if (directories.where((dir) => dir.name == title).length > 1 && dir.parent?.isNotEmpty == true) { - title = '${dir.parent}/$title'; - } - return MapEntry(title, dir); - }).toList() - ..sort((a, b) => compareAsciiUpperCase(a.key, b.key)); - metadataNotifier.value = Map.fromEntries(titledDirectories); - _loadedMetadataUri.value = entry.uri; - } else { - metadataNotifier.value = {}; - _loadedMetadataUri.value = null; + final rawTags = dirKV.value as Map; + return MetadataDirectory(directoryName, parent, _toSortedTags(rawTags)); + }).toList(); + + if (entry.isVideo || (entry.mimeType == MimeTypes.heif && entry.isMultiPage)) { + directories.addAll(await _getStreamDirectories()); } + + final titledDirectories = directories.map((dir) { + var title = dir.name; + if (directories.where((dir) => dir.name == title).length > 1 && dir.parent?.isNotEmpty == true) { + title = '${dir.parent}/$title'; + } + return MapEntry(title, dir); + }).toList() + ..sort((a, b) => compareAsciiUpperCase(a.key, b.key)); + metadataNotifier.value = Map.fromEntries(titledDirectories); _expandedDirectoryNotifier.value = null; } @@ -232,19 +213,19 @@ class _MetadataSectionSliverState extends State with Auto // group attachments by format (e.g. TTF fonts) if (attachmentStreams.isNotEmpty) { - final formatCount = >{}; + final formatCount = >{}; for (final stream in attachmentStreams) { - final codec = (stream[Keys.codecName] as String ?? 'unknown').toUpperCase(); + final codec = (stream[Keys.codecName] as String? ?? 'unknown').toUpperCase(); if (!formatCount.containsKey(codec)) { formatCount[codec] = []; } - formatCount[codec].add(stream[Keys.filename]); + formatCount[codec]!.add(stream[Keys.filename]); } if (formatCount.isNotEmpty) { final rawTags = formatCount.map((key, value) { final count = value.length; // remove duplicate names, so number of displayed names may not match displayed count - final names = value.where((v) => v != null).toSet().toList()..sort(compareAsciiUpperCase); + final names = value.where((v) => v != null).cast().toSet().toList()..sort(compareAsciiUpperCase); return MapEntry(key, '$count items: ${names.join(', ')}'); }); directories.add(MetadataDirectory('Attachments', null, _toSortedTags(rawTags))); @@ -255,23 +236,23 @@ class _MetadataSectionSliverState extends State with Auto } SplayTreeMap _toSortedTags(Map rawTags) { - final tags = SplayTreeMap.of(Map.fromEntries(rawTags.entries.map((tagKV) { - final value = (tagKV.value as String ?? '').trim(); - if (value.isEmpty) return null; - final tagName = tagKV.key as String ?? ''; - return MapEntry(tagName, value); - }).where((kv) => kv != null))); + final tags = SplayTreeMap.of(Map.fromEntries(rawTags.entries + .map((tagKV) { + var value = (tagKV.value as String? ?? '').trim(); + if (value.isEmpty) return null; + final tagName = tagKV.key as String; + return MapEntry(tagName, value); + }) + .where((kv) => kv != null) + .cast>())); return tags; } - - @override - bool get wantKeepAlive => true; } class MetadataDirectory { final String name; - final Color color; - final String parent; + final Color? color; + final String? parent; final SplayTreeMap allTags; final SplayTreeMap tags; @@ -281,7 +262,7 @@ class MetadataDirectory { static const mediaDirectory = 'Media'; // custom static const coverDirectory = 'Cover'; // custom - const MetadataDirectory(this.name, this.parent, SplayTreeMap allTags, {SplayTreeMap tags, this.color}) + const MetadataDirectory(this.name, this.parent, SplayTreeMap allTags, {SplayTreeMap? tags, this.color}) : allTags = allTags, tags = tags ?? allTags; diff --git a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart index 3c2c55005..0df49a5c2 100644 --- a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart +++ b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart @@ -10,8 +10,8 @@ class MetadataThumbnails extends StatefulWidget { final AvesEntry entry; const MetadataThumbnails({ - Key key, - @required this.entry, + Key? key, + required this.entry, }) : super(key: key); @override @@ -19,7 +19,7 @@ class MetadataThumbnails extends StatefulWidget { } class _MetadataThumbnailsState extends State { - Future> _loader; + late Future> _loader; AvesEntry get entry => widget.entry; @@ -36,12 +36,12 @@ class _MetadataThumbnailsState extends State { return FutureBuilder>( future: _loader, builder: (context, snapshot) { - if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data.isNotEmpty) { + if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data!.isNotEmpty) { return Container( alignment: AlignmentDirectional.topStart, - padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 4), + padding: const EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 4), child: Wrap( - children: snapshot.data.map((bytes) { + children: snapshot.data!.map((bytes) { return Image.memory( bytes, scale: context.select((mq) => mq.devicePixelRatio), @@ -50,7 +50,7 @@ class _MetadataThumbnailsState extends State { ), ); } - return SizedBox.shrink(); + return const SizedBox.shrink(); }); } } diff --git a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart index 613bd66ff..4856b63a6 100644 --- a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart +++ b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart @@ -57,13 +57,13 @@ class XmpNamespace { Map get buildProps => rawProps; List buildNamespaceSection() { - final props = buildProps - .entries + final props = buildProps.entries .map((kv) { final prop = XmpProp(kv.key, kv.value); return extractData(prop) ? null : prop; }) - .where((e) => e != null) + .where((v) => v != null) + .cast() .toList() ..sort((a, b) => compareAsciiUpperCaseNatural(a.displayKey, b.displayKey)); @@ -81,7 +81,7 @@ class XmpNamespace { ? [ if (displayTitle.isNotEmpty) Padding( - padding: EdgeInsets.only(top: 8), + padding: const EdgeInsets.only(top: 8), child: HighlightTitle( displayTitle, color: BrandColors.get(displayTitle), @@ -98,7 +98,7 @@ class XmpNamespace { if (matches.isEmpty) return false; final match = matches.first; - final field = XmpProp.formatKey(match.group(1)); + final field = XmpProp.formatKey(match.group(1)!); store[field] = formatValue(prop); return true; } @@ -108,8 +108,8 @@ class XmpNamespace { if (matches.isEmpty) return false; final match = matches.first; - final index = int.parse(match.group(1)); - final field = XmpProp.formatKey(match.group(2)); + final index = int.parse(match.group(1)!); + final field = XmpProp.formatKey(match.group(2)!); final fields = store.putIfAbsent(index, () => {}); fields[field] = formatValue(prop); return true; @@ -121,7 +121,7 @@ class XmpNamespace { String formatValue(XmpProp prop) => prop.value; - Map linkifyValues(List props) => null; + Map linkifyValues(List props) => {}; // identity diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart index 24db7a4da..ba305f4bf 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart @@ -2,6 +2,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; import 'package:aves/widgets/viewer/info/notifications.dart'; +import 'package:collection/collection.dart'; import 'package:tuple/tuple.dart'; abstract class XmpGoogleNamespace extends XmpNamespace { @@ -11,23 +12,26 @@ abstract class XmpGoogleNamespace extends XmpNamespace { @override Map linkifyValues(List props) { - return Map.fromEntries(dataProps.map((t) { - final dataPropPath = t.item1; - final mimePropPath = t.item2; - final dataProp = props.firstWhere((prop) => prop.path == dataPropPath, orElse: () => null); - final mimeProp = props.firstWhere((prop) => prop.path == mimePropPath, orElse: () => null); - return (dataProp != null && mimeProp != null) - ? MapEntry( - dataProp.displayKey, - InfoLinkHandler( - linkText: (context) => context.l10n.viewerInfoOpenLinkText, - onTap: (context) => OpenEmbeddedDataNotification.xmp( - propPath: dataProp.path, - mimeType: mimeProp.value, - ).dispatch(context), - )) - : null; - }).where((e) => e != null)); + return Map.fromEntries(dataProps + .map((t) { + final dataPropPath = t.item1; + final mimePropPath = t.item2; + final dataProp = props.firstWhereOrNull((prop) => prop.path == dataPropPath); + final mimeProp = props.firstWhereOrNull((prop) => prop.path == mimePropPath); + return (dataProp != null && mimeProp != null) + ? MapEntry( + dataProp.displayKey, + InfoLinkHandler( + linkText: (context) => context.l10n.viewerInfoOpenLinkText, + onTap: (context) => OpenEmbeddedDataNotification.xmp( + propPath: dataProp.path, + mimeType: mimeProp.value, + ).dispatch(context), + )) + : null; + }) + .where((kv) => kv != null) + .cast>()); } } @@ -37,7 +41,7 @@ class XmpGAudioNamespace extends XmpGoogleNamespace { XmpGAudioNamespace(Map rawProps) : super(ns, rawProps); @override - List> get dataProps => [Tuple2('$ns:Data', '$ns:Mime')]; + List> get dataProps => const [Tuple2('$ns:Data', '$ns:Mime')]; @override String get displayTitle => 'Google Audio'; @@ -49,7 +53,7 @@ class XmpGDepthNamespace extends XmpGoogleNamespace { XmpGDepthNamespace(Map rawProps) : super(ns, rawProps); @override - List> get dataProps => [ + List> get dataProps => const [ Tuple2('$ns:Data', '$ns:Mime'), Tuple2('$ns:Confidence', '$ns:ConfidenceMime'), ]; @@ -64,7 +68,7 @@ class XmpGImageNamespace extends XmpGoogleNamespace { XmpGImageNamespace(Map rawProps) : super(ns, rawProps); @override - List> get dataProps => [Tuple2('$ns:Data', '$ns:Mime')]; + List> get dataProps => const [Tuple2('$ns:Data', '$ns:Mime')]; @override String get displayTitle => 'Google Image'; @@ -75,7 +79,7 @@ class XmpGCameraNamespace extends XmpNamespace { static const videoOffsetKey = 'GCamera:MicroVideoOffset'; static const videoDataKey = 'Data'; - bool _isMotionPhoto; + late bool _isMotionPhoto; XmpGCameraNamespace(Map rawProps) : super(ns, rawProps) { _isMotionPhoto = rawProps.keys.any((key) => key == videoOffsetKey); @@ -85,7 +89,7 @@ class XmpGCameraNamespace extends XmpNamespace { Map get buildProps { return _isMotionPhoto ? Map.fromEntries({ - MapEntry(videoDataKey, '[skipped]'), + const MapEntry(videoDataKey, '[skipped]'), ...rawProps.entries, }) : rawProps; diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart index d39e5c1d4..6d007bcdb 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 { title: 'Thumbnail', structByIndex: thumbnails, linkifier: (index) { - final struct = thumbnails[index]; + final struct = thumbnails[index]!; return { if (struct.containsKey(thumbnailDataDisplayKey)) thumbnailDataDisplayKey: InfoLinkHandler( diff --git a/lib/widgets/viewer/info/metadata/xmp_structs.dart b/lib/widgets/viewer/info/metadata/xmp_structs.dart index 6060f46bf..aaff62184 100644 --- a/lib/widgets/viewer/info/metadata/xmp_structs.dart +++ b/lib/widgets/viewer/info/metadata/xmp_structs.dart @@ -12,15 +12,18 @@ import 'package:flutter/material.dart'; class XmpStructArrayCard extends StatefulWidget { final String title; final List> structs = []; - final Map Function(int index) linkifier; + final Map Function(int index)? linkifier; XmpStructArrayCard({ - @required this.title, - @required Map> structByIndex, + required this.title, + required Map> structByIndex, this.linkifier, }) { - structs.length = structByIndex.keys.fold(0, max); - structByIndex.keys.forEach((index) => structs[index - 1] = structByIndex[index]); + final length = structByIndex.keys.fold(0, max); + structs.length = length; + for (var i = 0; i < length; i++) { + structs[i] = structByIndex[i + 1] ?? {}; + } } @override @@ -28,7 +31,7 @@ class XmpStructArrayCard extends StatefulWidget { } class _XmpStructArrayCardState extends State { - int _index; + late int _index; List> get structs => widget.structs; @@ -54,7 +57,7 @@ class _XmpStructArrayCardState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.only(left: 8, top: 8, right: 8), + padding: const EdgeInsets.only(left: 8, top: 8, right: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -67,13 +70,13 @@ class _XmpStructArrayCardState extends State { ), IconButton( visualDensity: VisualDensity.compact, - icon: Icon(AIcons.previous), + icon: const Icon(AIcons.previous), onPressed: _index > 0 ? () => setIndex(_index - 1) : null, tooltip: context.l10n.previousTooltip, ), IconButton( visualDensity: VisualDensity.compact, - icon: Icon(AIcons.next), + icon: const Icon(AIcons.next), onPressed: _index < structs.length - 1 ? () => setIndex(_index + 1) : null, tooltip: context.l10n.nextTooltip, ), @@ -88,9 +91,9 @@ class _XmpStructArrayCardState extends State { // add padding at this level (instead of the column level) // so that the crossfader can animate the content size // without clipping the text - padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), child: InfoRowGroup( - structs[_index] ?? {}, + structs[_index], maxValueLength: Constants.infoGroupMaxValueLength, linkHandlers: widget.linkifier?.call(_index + 1), ), @@ -105,13 +108,13 @@ 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({ - @required this.title, - @required this.struct, + required this.title, + required this.struct, this.linkifier, }); @@ -120,7 +123,7 @@ class XmpStructCard extends StatelessWidget { return Card( margin: cardMargin, child: Padding( - padding: EdgeInsets.all(8), + padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/widgets/viewer/info/metadata/xmp_tile.dart b/lib/widgets/viewer/info/metadata/xmp_tile.dart index 1663aac44..387d00531 100644 --- a/lib/widgets/viewer/info/metadata/xmp_tile.dart +++ b/lib/widgets/viewer/info/metadata/xmp_tile.dart @@ -10,14 +10,14 @@ import 'package:flutter/material.dart'; class XmpDirTile extends StatefulWidget { final AvesEntry entry; final SplayTreeMap tags; - final ValueNotifier expandedNotifier; + final ValueNotifier? expandedNotifier; final bool initiallyExpanded; const XmpDirTile({ - @required this.entry, - @required this.tags, - @required this.expandedNotifier, - @required this.initiallyExpanded, + required this.entry, + required this.tags, + required this.expandedNotifier, + required this.initiallyExpanded, }); @override @@ -29,7 +29,7 @@ class _XmpDirTileState extends State { @override Widget build(BuildContext context) { - final sections = groupBy(widget.tags.entries, (kv) { + final sections = groupBy, String>(widget.tags.entries, (kv) { final fullKey = kv.key; final i = fullKey.indexOf(XMP.propNamespaceSeparator); final namespace = i == -1 ? '' : fullKey.substring(0, i); @@ -42,7 +42,7 @@ class _XmpDirTileState extends State { initiallyExpanded: widget.initiallyExpanded, children: [ Padding( - padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: sections.expand((section) => section.buildNamespaceSection()).toList(), diff --git a/lib/widgets/viewer/info/notifications.dart b/lib/widgets/viewer/info/notifications.dart index c5d23c7a3..54a0ccfa3 100644 --- a/lib/widgets/viewer/info/notifications.dart +++ b/lib/widgets/viewer/info/notifications.dart @@ -21,7 +21,7 @@ class OpenTempEntryNotification extends Notification { final AvesEntry entry; const OpenTempEntryNotification({ - @required this.entry, + required this.entry, }); @override @@ -32,26 +32,26 @@ enum EmbeddedDataSource { motionPhotoVideo, videoCover, xmp } class OpenEmbeddedDataNotification extends Notification { final EmbeddedDataSource source; - final String propPath; - final String mimeType; + final String? propPath; + final String? mimeType; const OpenEmbeddedDataNotification._private({ - @required this.source, + required this.source, this.propPath, this.mimeType, }); - factory OpenEmbeddedDataNotification.motionPhotoVideo() => OpenEmbeddedDataNotification._private( + factory OpenEmbeddedDataNotification.motionPhotoVideo() => const OpenEmbeddedDataNotification._private( source: EmbeddedDataSource.motionPhotoVideo, ); - factory OpenEmbeddedDataNotification.videoCover() => OpenEmbeddedDataNotification._private( + factory OpenEmbeddedDataNotification.videoCover() => const OpenEmbeddedDataNotification._private( source: EmbeddedDataSource.videoCover, ); factory OpenEmbeddedDataNotification.xmp({ - @required String propPath, - @required String mimeType, + required String propPath, + required String mimeType, }) => OpenEmbeddedDataNotification._private( source: EmbeddedDataSource.xmp, diff --git a/lib/widgets/viewer/multipage/conductor.dart b/lib/widgets/viewer/multipage/conductor.dart index 709c3a711..da6915149 100644 --- a/lib/widgets/viewer/multipage/conductor.dart +++ b/lib/widgets/viewer/multipage/conductor.dart @@ -1,5 +1,6 @@ import 'package:aves/model/entry.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; +import 'package:collection/collection.dart'; class MultiPageConductor { final List _controllers = []; @@ -7,7 +8,7 @@ class MultiPageConductor { static const maxControllerCount = 3; Future dispose() async { - await Future.forEach(_controllers, (controller) => controller.dispose()); + await Future.forEach(_controllers, (controller) => controller.dispose()); _controllers.clear(); } @@ -25,7 +26,7 @@ class MultiPageConductor { return controller; } - MultiPageController getController(AvesEntry entry) { - return _controllers.firstWhere((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId, orElse: () => null); + MultiPageController? getController(AvesEntry entry) { + return _controllers.firstWhereOrNull((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId); } } diff --git a/lib/widgets/viewer/multipage/controller.dart b/lib/widgets/viewer/multipage/controller.dart index 00224f8f5..66c8705fb 100644 --- a/lib/widgets/viewer/multipage/controller.dart +++ b/lib/widgets/viewer/multipage/controller.dart @@ -8,29 +8,32 @@ import 'package:flutter/material.dart'; class MultiPageController { final AvesEntry entry; - final ValueNotifier pageNotifier = ValueNotifier(null); + final ValueNotifier pageNotifier = ValueNotifier(null); - MultiPageInfo _info; + bool _disposed = false; + MultiPageInfo? _info; - final StreamController _infoStreamController = StreamController.broadcast(); + final StreamController _infoStreamController = StreamController.broadcast(); - Stream get infoStream => _infoStreamController.stream; + Stream get infoStream => _infoStreamController.stream; - MultiPageInfo get info => _info; + MultiPageInfo? get info => _info; - int get page => pageNotifier.value; + int? get page => pageNotifier.value; - set page(int page) => pageNotifier.value = page; + set page(int? page) => pageNotifier.value = page; MultiPageController(this.entry) { metadataService.getMultiPageInfo(entry).then((value) { - pageNotifier.value = value.defaultPage.index; + if (value == null || _disposed) return; + pageNotifier.value = value.defaultPage!.index; _info = value; _infoStreamController.add(_info); }); } void dispose() { + _disposed = true; pageNotifier.dispose(); } diff --git a/lib/widgets/viewer/overlay/bottom/common.dart b/lib/widgets/viewer/overlay/bottom/common.dart index ae870bd3b..47caa371a 100644 --- a/lib/widgets/viewer/overlay/bottom/common.dart +++ b/lib/widgets/viewer/overlay/bottom/common.dart @@ -15,6 +15,7 @@ import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/overlay/bottom/multipage.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:decorated_icon/decorated_icon.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; @@ -24,17 +25,17 @@ class ViewerBottomOverlay extends StatefulWidget { final List entries; final int index; final bool showPosition; - final EdgeInsets viewInsets, viewPadding; - final MultiPageController multiPageController; + final EdgeInsets? viewInsets, viewPadding; + final MultiPageController? multiPageController; const ViewerBottomOverlay({ - Key key, - @required this.entries, - @required this.index, - @required this.showPosition, + Key? key, + required this.entries, + required this.index, + required this.showPosition, this.viewInsets, this.viewPadding, - @required this.multiPageController, + required this.multiPageController, }) : super(key: key); @override @@ -42,17 +43,17 @@ class ViewerBottomOverlay extends StatefulWidget { } class _ViewerBottomOverlayState extends State { - Future _detailLoader; - AvesEntry _lastEntry; - OverlayMetadata _lastDetails; + late Future _detailLoader; + AvesEntry? _lastEntry; + OverlayMetadata? _lastDetails; - AvesEntry get entry { + AvesEntry? get entry { final entries = widget.entries; final index = widget.index; return index < entries.length ? entries[index] : null; } - MultiPageController get multiPageController => widget.multiPageController; + MultiPageController? get multiPageController => widget.multiPageController; @override void initState() { @@ -69,7 +70,8 @@ class _ViewerBottomOverlayState extends State { } void _initDetailLoader() { - _detailLoader = metadataService.getOverlayMetadata(entry); + final requestEntry = entry; + _detailLoader = requestEntry != null ? metadataService.getOverlayMetadata(requestEntry) : SynchronousFuture(null); } @override @@ -90,19 +92,24 @@ class _ViewerBottomOverlayState extends State { return Container( color: hasEdgeContent ? kOverlayBackgroundColor : Colors.transparent, - padding: viewInsets + viewPadding.copyWith(top: 0), - child: FutureBuilder( + padding: EdgeInsets.only( + left: max(viewInsets.left, viewPadding.left), + top: 0, + right: max(viewInsets.right, viewPadding.right), + bottom: max(viewInsets.bottom, viewPadding.bottom), + ), + child: FutureBuilder( future: _detailLoader, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) { _lastDetails = snapshot.data; _lastEntry = entry; } - if (_lastEntry == null) return SizedBox.shrink(); + if (_lastEntry == null) return const SizedBox.shrink(); - Widget _buildContent({MultiPageInfo multiPageInfo, int page}) => _BottomOverlayContent( - mainEntry: _lastEntry, - pageEntry: multiPageInfo?.getPageEntryByIndex(page) ?? _lastEntry, + Widget _buildContent({MultiPageInfo? multiPageInfo, int? page}) => _BottomOverlayContent( + mainEntry: _lastEntry!, + pageEntry: multiPageInfo?.getPageEntryByIndex(page) ?? _lastEntry!, details: _lastDetails, position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null, availableWidth: availableWidth, @@ -111,12 +118,12 @@ class _ViewerBottomOverlayState extends State { if (multiPageController == null) return _buildContent(); - return StreamBuilder( - stream: multiPageController.infoStream, + return StreamBuilder( + stream: multiPageController!.infoStream, builder: (context, snapshot) { - final multiPageInfo = multiPageController.info; - return ValueListenableBuilder( - valueListenable: multiPageController.pageNotifier, + final multiPageInfo = multiPageController!.info; + return ValueListenableBuilder( + valueListenable: multiPageController!.pageNotifier, builder: (context, page, child) { return _buildContent(multiPageInfo: multiPageInfo, page: page); }, @@ -139,20 +146,20 @@ const double _subRowMinWidth = 300.0; class _BottomOverlayContent extends AnimatedWidget { final AvesEntry mainEntry, pageEntry; - final OverlayMetadata details; - final String position; + final OverlayMetadata? details; + final String? position; final double availableWidth; - final MultiPageController multiPageController; + final MultiPageController? multiPageController; static const infoPadding = EdgeInsets.symmetric(vertical: 4, horizontal: 8); _BottomOverlayContent({ - Key key, - this.mainEntry, - this.pageEntry, + Key? key, + required this.mainEntry, + required this.pageEntry, this.details, this.position, - this.availableWidth, + required this.availableWidth, this.multiPageController, }) : super( key: key, @@ -165,9 +172,9 @@ class _BottomOverlayContent extends AnimatedWidget { @override Widget build(BuildContext context) { return DefaultTextStyle( - style: Theme.of(context).textTheme.bodyText2.copyWith( - shadows: [Constants.embossShadow], - ), + style: Theme.of(context).textTheme.bodyText2!.copyWith( + shadows: Constants.embossShadows, + ), softWrap: false, overflow: TextOverflow.fade, maxLines: 1, @@ -176,7 +183,7 @@ class _BottomOverlayContent extends AnimatedWidget { child: Selector( selector: (c, mq) => mq.orientation, builder: (c, orientation, child) { - Widget infoColumn; + Widget? infoColumn; if (settings.showOverlayInfo) { infoColumn = _buildInfoColumn(orientation); @@ -188,7 +195,7 @@ class _BottomOverlayContent extends AnimatedWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ MultiPageOverlay( - controller: multiPageController, + controller: multiPageController!, availableWidth: availableWidth, ), if (infoColumn != null) infoColumn, @@ -196,7 +203,7 @@ class _BottomOverlayContent extends AnimatedWidget { ); } - return infoColumn ?? SizedBox(); + return infoColumn ?? const SizedBox(); }, ), ), @@ -208,7 +215,7 @@ class _BottomOverlayContent extends AnimatedWidget { final twoColumns = orientation == Orientation.landscape && infoMaxWidth / 2 > _subRowMinWidth; final subRowWidth = twoColumns ? min(_subRowMinWidth, infoMaxWidth / 2) : infoMaxWidth; final positionTitle = _PositionTitleRow(entry: pageEntry, collectionPosition: position, multiPageController: multiPageController); - final hasShootingDetails = details != null && !details.isEmpty && settings.showOverlayShootingDetails; + final hasShootingDetails = details != null && !details!.isEmpty && settings.showOverlayShootingDetails; return Padding( padding: infoPadding, @@ -220,7 +227,7 @@ class _BottomOverlayContent extends AnimatedWidget { _buildSoloLocationRow(), if (twoColumns) Padding( - padding: EdgeInsets.only(top: _interRowPadding), + padding: const EdgeInsets.only(top: _interRowPadding), child: Row( children: [ Container( @@ -235,7 +242,7 @@ class _BottomOverlayContent extends AnimatedWidget { ) else ...[ Container( - padding: EdgeInsets.only(top: _interRowPadding), + padding: const EdgeInsets.only(top: _interRowPadding), width: subRowWidth, child: _DateRow( entry: pageEntry, @@ -256,10 +263,10 @@ class _BottomOverlayContent extends AnimatedWidget { transitionBuilder: _soloTransition, child: pageEntry.hasGps ? Container( - padding: EdgeInsets.only(top: _interRowPadding), + padding: const EdgeInsets.only(top: _interRowPadding), child: _LocationRow(entry: pageEntry), ) - : SizedBox.shrink(), + : const SizedBox.shrink(), ); Widget _buildSoloShootingRow(double subRowWidth, bool hasShootingDetails) => AnimatedSwitcher( @@ -269,11 +276,11 @@ class _BottomOverlayContent extends AnimatedWidget { transitionBuilder: _soloTransition, child: hasShootingDetails ? Container( - padding: EdgeInsets.only(top: _interRowPadding), + padding: const EdgeInsets.only(top: _interRowPadding), width: subRowWidth, - child: _ShootingRow(details), + child: _ShootingRow(details!), ) - : SizedBox.shrink(), + : const SizedBox.shrink(), ); Widget _buildDuoShootingRow(double subRowWidth, bool hasShootingDetails) => AnimatedSwitcher( @@ -287,9 +294,9 @@ class _BottomOverlayContent extends AnimatedWidget { child: hasShootingDetails ? Container( width: subRowWidth, - child: _ShootingRow(details), + child: _ShootingRow(details!), ) - : SizedBox.shrink(), + : const SizedBox.shrink(), ); static Widget _soloTransition(Widget child, Animation animation) => FadeTransition( @@ -306,22 +313,17 @@ class _LocationRow extends AnimatedWidget { final AvesEntry entry; _LocationRow({ - Key key, - this.entry, + Key? key, + required this.entry, }) : super(key: key, listenable: entry.addressChangeNotifier); @override Widget build(BuildContext context) { - String location; - if (entry.hasAddress) { - location = entry.shortAddress; - } else if (entry.hasGps) { - location = settings.coordinateFormat.format(entry.latLng); - } + final location = entry.hasAddress ? entry.shortAddress : settings.coordinateFormat.format(entry.latLng!); return Row( children: [ - DecoratedIcon(AIcons.location, shadows: [Constants.embossShadow], size: _iconSize), - SizedBox(width: _iconPadding), + const DecoratedIcon(AIcons.location, shadows: Constants.embossShadows, size: _iconSize), + const SizedBox(width: _iconPadding), Expanded(child: Text(location, strutStyle: Constants.overflowStrutStyle)), ], ); @@ -330,16 +332,16 @@ class _LocationRow extends AnimatedWidget { class _PositionTitleRow extends StatelessWidget { final AvesEntry entry; - final String collectionPosition; - final MultiPageController multiPageController; + final String? collectionPosition; + final MultiPageController? multiPageController; const _PositionTitleRow({ - @required this.entry, - @required this.collectionPosition, - @required this.multiPageController, + required this.entry, + required this.collectionPosition, + required this.multiPageController, }); - String get title => entry.bestTitle; + String? get title => entry.bestTitle; bool get isNotEmpty => collectionPosition != null || multiPageController != null || title != null; @@ -347,7 +349,7 @@ class _PositionTitleRow extends StatelessWidget { @override Widget build(BuildContext context) { - Text toText({String pagePosition}) => Text( + Text toText({String? pagePosition}) => Text( [ if (collectionPosition != null) collectionPosition, if (pagePosition != null) pagePosition, @@ -357,11 +359,11 @@ class _PositionTitleRow extends StatelessWidget { if (multiPageController == null) return toText(); - return StreamBuilder( - stream: multiPageController.infoStream, + return StreamBuilder( + stream: multiPageController!.infoStream, builder: (context, snapshot) { - final multiPageInfo = multiPageController.info; - String pagePosition; + final multiPageInfo = multiPageController!.info; + String? pagePosition; if (multiPageInfo != null) { // page count may be 0 when we know an entry to have multiple pages // but fail to get information about these pages @@ -379,11 +381,11 @@ class _PositionTitleRow extends StatelessWidget { class _DateRow extends StatelessWidget { final AvesEntry entry; - final MultiPageController multiPageController; + final MultiPageController? multiPageController; const _DateRow({ - @required this.entry, - @required this.multiPageController, + required this.entry, + required this.multiPageController, }); @override @@ -399,8 +401,8 @@ class _DateRow extends StatelessWidget { return Row( children: [ - DecoratedIcon(AIcons.date, shadows: [Constants.embossShadow], size: _iconSize), - SizedBox(width: _iconPadding), + const DecoratedIcon(AIcons.date, shadows: Constants.embossShadows, size: _iconSize), + const SizedBox(width: _iconPadding), Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)), Expanded(flex: 2, child: Text(resolutionText, strutStyle: Constants.overflowStrutStyle)), ], @@ -417,8 +419,8 @@ class _ShootingRow extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - DecoratedIcon(AIcons.shooting, shadows: [Constants.embossShadow], size: _iconSize), - SizedBox(width: _iconPadding), + const DecoratedIcon(AIcons.shooting, shadows: Constants.embossShadows, size: _iconSize), + const SizedBox(width: _iconPadding), Expanded(child: Text(details.aperture ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)), Expanded(child: Text(details.exposureTime ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)), Expanded(child: Text(details.focalLength ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)), @@ -429,14 +431,14 @@ class _ShootingRow extends StatelessWidget { } class ExtraBottomOverlay extends StatelessWidget { - final EdgeInsets viewInsets, viewPadding; + final EdgeInsets? viewInsets, viewPadding; final Widget child; const ExtraBottomOverlay({ - Key key, + Key? key, this.viewInsets, this.viewPadding, - @required this.child, + required this.child, }) : super(key: key); @override @@ -448,7 +450,7 @@ class ExtraBottomOverlay extends StatelessWidget { final viewInsets = this.viewInsets ?? mqViewInsets; final viewPadding = this.viewPadding ?? mqViewPadding; - final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + EdgeInsets.symmetric(horizontal: 8.0); + final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + const EdgeInsets.symmetric(horizontal: 8.0); return Padding( padding: safePadding, diff --git a/lib/widgets/viewer/overlay/bottom/multipage.dart b/lib/widgets/viewer/overlay/bottom/multipage.dart index 0ac68df62..b0817a449 100644 --- a/lib/widgets/viewer/overlay/bottom/multipage.dart +++ b/lib/widgets/viewer/overlay/bottom/multipage.dart @@ -13,11 +13,10 @@ class MultiPageOverlay extends StatefulWidget { final double availableWidth; const MultiPageOverlay({ - Key key, - @required this.controller, - @required this.availableWidth, - }) : assert(controller != null), - super(key: key); + Key? key, + required this.controller, + required this.availableWidth, + }) : super(key: key); @override _MultiPageOverlayState createState() => _MultiPageOverlayState(); @@ -25,9 +24,9 @@ class MultiPageOverlay extends StatefulWidget { class _MultiPageOverlayState extends State { final _cancellableNotifier = ValueNotifier(true); - ScrollController _scrollController; + late ScrollController _scrollController; bool _syncScroll = true; - int _initControllerPage; + int? _initControllerPage; static const double extent = 48; static const double separatorWidth = 2; @@ -75,8 +74,8 @@ class _MultiPageOverlayState extends State { await controller.infoStream.first; if (_initControllerPage == null) { _initControllerPage = controller.page; - if (_initControllerPage != 0) { - WidgetsBinding.instance.addPostFrameCallback((_) => _goToPage(_initControllerPage)); + if (_initControllerPage != null && _initControllerPage != 0) { + WidgetsBinding.instance!.addPostFrameCallback((_) => _goToPage(_initControllerPage!)); } } } @@ -88,14 +87,14 @@ class _MultiPageOverlayState extends State { @override Widget build(BuildContext context) { - final marginWidth = max(0, (availableWidth - extent) / 2 - separatorWidth); + final marginWidth = max(0.0, (availableWidth - extent) / 2 - separatorWidth); final horizontalMargin = SizedBox(width: marginWidth); - final separator = SizedBox(width: separatorWidth); + const separator = SizedBox(width: separatorWidth); return ThumbnailTheme( extent: extent, showLocation: false, - child: StreamBuilder( + child: StreamBuilder( stream: controller.infoStream, builder: (context, snapshot) { final multiPageInfo = controller.info; @@ -112,7 +111,7 @@ class _MultiPageOverlayState extends State { itemBuilder: (context, index) { if (index == 0 || index == pageCount + 1) return horizontalMargin; final page = index - 1; - final pageEntry = multiPageInfo.getPageEntryByIndex(page); + final pageEntry = multiPageInfo!.getPageEntryByIndex(page); return Stack( children: [ @@ -120,7 +119,7 @@ class _MultiPageOverlayState extends State { onTap: () => _goToPage(page), child: DecoratedThumbnail( entry: pageEntry, - extent: extent, + tileExtent: extent, // the retrieval task queue can pile up for thumbnails of heavy pages // (e.g. thumbnails of 15MP HEIF images inside 100MB+ HEIC containers) // so we cancel these requests when possible diff --git a/lib/widgets/viewer/overlay/bottom/panorama.dart b/lib/widgets/viewer/overlay/bottom/panorama.dart index 4998f86c8..861afbae2 100644 --- a/lib/widgets/viewer/overlay/bottom/panorama.dart +++ b/lib/widgets/viewer/overlay/bottom/panorama.dart @@ -11,16 +11,16 @@ class PanoramaOverlay extends StatelessWidget { final Animation scale; const PanoramaOverlay({ - Key key, - @required this.entry, - @required this.scale, + Key? key, + required this.entry, + required this.scale, }) : super(key: key); @override Widget build(BuildContext context) { return Row( children: [ - Spacer(), + const Spacer(), OverlayTextButton( scale: scale, buttonLabel: context.l10n.viewerOpenPanoramaButtonLabel, @@ -30,7 +30,7 @@ class PanoramaOverlay extends StatelessWidget { unawaited(Navigator.push( context, MaterialPageRoute( - settings: RouteSettings(name: PanoramaPage.routeName), + settings: const RouteSettings(name: PanoramaPage.routeName), builder: (context) => PanoramaPage( entry: entry, info: info, diff --git a/lib/widgets/viewer/overlay/bottom/video.dart b/lib/widgets/viewer/overlay/bottom/video.dart index 8327a1c74..7ba521522 100644 --- a/lib/widgets/viewer/overlay/bottom/video.dart +++ b/lib/widgets/viewer/overlay/bottom/video.dart @@ -15,14 +15,14 @@ import 'package:flutter/material.dart'; class VideoControlOverlay extends StatefulWidget { final AvesEntry entry; - final AvesVideoController controller; + final AvesVideoController? controller; final Animation scale; const VideoControlOverlay({ - Key key, - @required this.entry, - @required this.controller, - @required this.scale, + Key? key, + required this.entry, + required this.controller, + required this.scale, }) : super(key: key); @override @@ -32,14 +32,14 @@ class VideoControlOverlay extends StatefulWidget { class _VideoControlOverlayState extends State with SingleTickerProviderStateMixin { final GlobalKey _progressBarKey = GlobalKey(debugLabel: 'video-progress-bar'); bool _playingOnDragStart = false; - AnimationController _playPauseAnimation; + late AnimationController _playPauseAnimation; final List _subscriptions = []; AvesEntry get entry => widget.entry; Animation get scale => widget.scale; - AvesVideoController get controller => widget.controller; + AvesVideoController? get controller => widget.controller; Stream get statusStream => controller?.statusStream ?? Stream.value(VideoStatus.idle); @@ -72,9 +72,10 @@ class _VideoControlOverlayState extends State with SingleTi } void _registerWidget(VideoControlOverlay widget) { - if (widget.controller != null) { - _subscriptions.add(widget.controller.statusStream.listen(_onStatusChange)); - _onStatusChange(widget.controller.status); + final controller = widget.controller; + if (controller != null) { + _subscriptions.add(controller.statusStream.listen(_onStatusChange)); + _onStatusChange(controller.status); } } @@ -102,7 +103,7 @@ class _VideoControlOverlayState extends State with SingleTi OverlayButton( scale: scale, child: IconButton( - icon: Icon(AIcons.openOutside), + icon: const Icon(AIcons.openOutside), onPressed: () => AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype), tooltip: context.l10n.viewerOpenTooltip, ), @@ -112,7 +113,7 @@ class _VideoControlOverlayState extends State with SingleTi Expanded( child: _buildProgressBar(), ), - SizedBox(width: 8), + const SizedBox(width: 8), OverlayButton( scale: scale, child: IconButton( @@ -142,20 +143,20 @@ class _VideoControlOverlayState extends State with SingleTi }, onHorizontalDragStart: (details) { _playingOnDragStart = isPlaying; - if (_playingOnDragStart) controller.pause(); + if (_playingOnDragStart) controller!.pause(); }, onHorizontalDragUpdate: (details) { _seekFromTap(details.globalPosition); }, onHorizontalDragEnd: (details) { - if (_playingOnDragStart) controller.play(); + if (_playingOnDragStart) controller!.play(); }, child: Container( - padding: EdgeInsets.symmetric(vertical: 4, horizontal: 16) + EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16) + const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: kOverlayBackgroundColor, - border: AvesCircleBorder.build(context), - borderRadius: BorderRadius.circular(progressBarBorderRadius), + border: AvesBorder.border, + borderRadius: const BorderRadius.all(Radius.circular(progressBarBorderRadius)), ), child: Column( key: _progressBarKey, @@ -166,15 +167,15 @@ class _VideoControlOverlayState extends State with SingleTi stream: positionStream, builder: (context, snapshot) { // do not use stream snapshot because it is obsolete when switching between videos - final position = controller?.currentPosition?.floor() ?? 0; + final position = controller?.currentPosition.floor() ?? 0; return Text(formatFriendlyDuration(Duration(milliseconds: position))); }), - Spacer(), + const Spacer(), Text(entry.durationText), ], ), ClipRRect( - borderRadius: BorderRadius.circular(4), + borderRadius: const BorderRadius.all(Radius.circular(4)), child: StreamBuilder( stream: positionStream, builder: (context, snapshot) { @@ -183,7 +184,7 @@ class _VideoControlOverlayState extends State with SingleTi if (!progress.isFinite) progress = 0.0; return LinearProgressIndicator( value: progress, - backgroundColor: Colors.grey[700], + backgroundColor: Colors.grey.shade700, ); }), ), @@ -207,9 +208,9 @@ class _VideoControlOverlayState extends State with SingleTi Future _togglePlayPause() async { if (controller == null) return; if (isPlaying) { - await controller.pause(); + await controller!.pause(); } else { - await controller.play(); + await controller!.play(); // hide overlay await Future.delayed(Durations.iconAnimation); ToggleOverlayNotification().dispatch(context); @@ -218,9 +219,9 @@ class _VideoControlOverlayState extends State with SingleTi void _seekFromTap(Offset globalPosition) async { if (controller == null) return; - final keyContext = _progressBarKey.currentContext; - final RenderBox box = keyContext.findRenderObject(); + final keyContext = _progressBarKey.currentContext!; + final box = keyContext.findRenderObject() as RenderBox; final localPosition = box.globalToLocal(globalPosition); - await controller.seekToProgress(localPosition.dx / box.size.width); + await controller!.seekToProgress(localPosition.dx / box.size.width); } } diff --git a/lib/widgets/viewer/overlay/common.dart b/lib/widgets/viewer/overlay/common.dart index e2bcdd9d6..03b62c557 100644 --- a/lib/widgets/viewer/overlay/common.dart +++ b/lib/widgets/viewer/overlay/common.dart @@ -9,9 +9,9 @@ class OverlayButton extends StatelessWidget { final Widget child; const OverlayButton({ - Key key, + Key? key, this.scale = kAlwaysCompleteAnimation, - @required this.child, + required this.child, }) : super(key: key); @override @@ -24,7 +24,7 @@ class OverlayButton extends StatelessWidget { color: kOverlayBackgroundColor, child: Ink( decoration: BoxDecoration( - border: AvesCircleBorder.build(context), + border: AvesBorder.border, shape: BoxShape.circle, ), child: child, @@ -41,18 +41,17 @@ class OverlayButton extends StatelessWidget { class OverlayTextButton extends StatelessWidget { final Animation scale; final String buttonLabel; - final VoidCallback onPressed; + final VoidCallback? onPressed; const OverlayTextButton({ - Key key, - @required this.scale, - @required this.buttonLabel, + Key? key, + required this.scale, + required this.buttonLabel, this.onPressed, - }) : assert(scale != null), - super(key: key); + }) : super(key: key); static const _borderRadius = 123.0; - static final _minSize = MaterialStateProperty.all(Size(kMinInteractiveDimension, kMinInteractiveDimension)); + static final _minSize = MaterialStateProperty.all(const Size(kMinInteractiveDimension, kMinInteractiveDimension)); @override Widget build(BuildContext context) { @@ -67,9 +66,9 @@ class OverlayTextButton extends StatelessWidget { foregroundColor: MaterialStateProperty.all(Colors.white), overlayColor: MaterialStateProperty.all(Colors.white.withOpacity(0.12)), minimumSize: _minSize, - side: MaterialStateProperty.all(AvesCircleBorder.buildSide(context)), - shape: MaterialStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(_borderRadius), + side: MaterialStateProperty.all(AvesBorder.side), + shape: MaterialStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(_borderRadius)), )), // shape: MaterialStateProperty.all(CircleBorder()), ), diff --git a/lib/widgets/viewer/overlay/minimap.dart b/lib/widgets/viewer/overlay/minimap.dart index 0f1431e39..e8c2845de 100644 --- a/lib/widgets/viewer/overlay/minimap.dart +++ b/lib/widgets/viewer/overlay/minimap.dart @@ -1,9 +1,11 @@ import 'dart:math'; import 'package:aves/model/entry.dart'; +import 'package:aves/widgets/viewer/video/conductor.dart'; import 'package:aves/widgets/viewer/visual/state.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class Minimap extends StatelessWidget { final AvesEntry entry; @@ -13,8 +15,8 @@ class Minimap extends StatelessWidget { static const defaultSize = Size(96, 96); const Minimap({ - @required this.entry, - @required this.viewStateNotifier, + required this.entry, + required this.viewStateNotifier, this.size = defaultSize, }); @@ -25,19 +27,33 @@ class Minimap extends StatelessWidget { valueListenable: viewStateNotifier, builder: (context, viewState, child) { final viewportSize = viewState.viewportSize; - if (viewportSize == null) return SizedBox.shrink(); + if (viewportSize == null) return const SizedBox.shrink(); return AnimatedBuilder( animation: entry.imageChangeNotifier, - builder: (context, child) => CustomPaint( - painter: MinimapPainter( - viewportSize: viewportSize, - entrySize: entry.displaySize, - viewCenterOffset: viewState.position, - viewScale: viewState.scale, - minimapBorderColor: Colors.white30, - ), - size: size, - ), + builder: (context, child) { + Widget _builder(Size displaySize) => CustomPaint( + painter: MinimapPainter( + viewportSize: viewportSize, + entrySize: displaySize, + viewCenterOffset: viewState.position, + viewScale: viewState.scale!, + minimapBorderColor: Colors.white30, + ), + size: size, + ); + + if (entry.isVideo) { + final videoController = context.read().getController(entry); + if (videoController == null) return const SizedBox(); + return ValueListenableBuilder( + valueListenable: videoController.sarNotifier, + builder: (context, sar, child) { + return _builder(entry.videoDisplaySize(sar)); + }, + ); + } + return _builder(entry.displaySize); + }, ); }), ); @@ -51,16 +67,13 @@ class MinimapPainter extends CustomPainter { final Color minimapBorderColor, viewportBorderColor; const MinimapPainter({ - @required this.viewportSize, - @required this.entrySize, - @required this.viewCenterOffset, - @required this.viewScale, + required this.viewportSize, + required this.entrySize, + required this.viewCenterOffset, + required this.viewScale, this.minimapBorderColor = Colors.white, this.viewportBorderColor = Colors.white, - }) : assert(viewportSize != null), - assert(entrySize != null), - assert(viewCenterOffset != null), - assert(viewScale != null); + }); @override void paint(Canvas canvas, Size size) { @@ -68,7 +81,7 @@ class MinimapPainter extends CustomPainter { if (viewSize.isEmpty) return; // hide minimap when image is in full view - if (viewportSize + Offset(precisionErrorTolerance, precisionErrorTolerance) >= viewSize) return; + if (viewportSize + const Offset(precisionErrorTolerance, precisionErrorTolerance) >= viewSize) return; final canvasScale = size.longestSide / viewSize.longestSide; final scaledEntrySize = viewSize * canvasScale; @@ -89,7 +102,7 @@ class MinimapPainter extends CustomPainter { final fill = Paint() ..style = PaintingStyle.fill - ..color = Color(0x33000000); + ..color = const Color(0x33000000); final minimapStroke = Paint() ..style = PaintingStyle.stroke ..color = minimapBorderColor; diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index e04854eb6..4e20982a2 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -20,22 +20,22 @@ import 'package:provider/provider.dart'; class ViewerTopOverlay extends StatelessWidget { final AvesEntry mainEntry; final Animation scale; - final EdgeInsets viewInsets, viewPadding; + final EdgeInsets? viewInsets, viewPadding; final Function(EntryAction value) onActionSelected; final bool canToggleFavourite; - final ValueNotifier viewStateNotifier; + final ValueNotifier? viewStateNotifier; static const double padding = 8; const ViewerTopOverlay({ - Key key, - @required this.mainEntry, - @required this.scale, - @required this.canToggleFavourite, - @required this.viewInsets, - @required this.viewPadding, - @required this.onActionSelected, - @required this.viewStateNotifier, + Key? key, + required this.mainEntry, + required this.scale, + required this.canToggleFavourite, + required this.viewInsets, + required this.viewPadding, + required this.onActionSelected, + required this.viewStateNotifier, }) : super(key: key); @override @@ -43,21 +43,21 @@ class ViewerTopOverlay extends StatelessWidget { return SafeArea( minimum: (viewInsets ?? EdgeInsets.zero) + (viewPadding ?? EdgeInsets.zero), child: Padding( - padding: EdgeInsets.all(padding), + padding: const EdgeInsets.all(padding), child: Selector( selector: (c, mq) => mq.size.width - mq.padding.horizontal, builder: (c, mqWidth, child) { final availableCount = (mqWidth / (OverlayButton.getSize(context) + padding)).floor() - 2; - Widget child; + Widget? child; if (mainEntry.isMultiPage) { final multiPageController = context.read().getController(mainEntry); if (multiPageController != null) { - child = StreamBuilder( + child = StreamBuilder( stream: multiPageController.infoStream, builder: (context, snapshot) { final multiPageInfo = multiPageController.info; - return ValueListenableBuilder( + return ValueListenableBuilder( valueListenable: multiPageController.pageNotifier, builder: (context, page, child) { return _buildOverlay(availableCount, mainEntry, pageEntry: multiPageInfo?.getPageEntryByIndex(page)); @@ -75,11 +75,11 @@ class ViewerTopOverlay extends StatelessWidget { ); } - Widget _buildOverlay(int availableCount, AvesEntry mainEntry, {AvesEntry pageEntry}) { + Widget _buildOverlay(int availableCount, AvesEntry mainEntry, {AvesEntry? pageEntry}) { pageEntry ??= mainEntry; bool _canDo(EntryAction action) { - final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry : mainEntry; + final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry! : mainEntry; switch (action) { case EntryAction.toggleFavourite: return canToggleFavourite; @@ -106,7 +106,6 @@ class ViewerTopOverlay extends StatelessWidget { case EntryAction.debug: return kDebugMode; } - return false; } final quickActions = settings.viewerQuickActions.where(_canDo).take(availableCount).toList(); @@ -127,12 +126,12 @@ class ViewerTopOverlay extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ buttonRow, - SizedBox(height: 8), + const SizedBox(height: 8), FadeTransition( opacity: scale, child: Minimap( entry: pageEntry, - viewStateNotifier: viewStateNotifier, + viewStateNotifier: viewStateNotifier!, ), ) ], @@ -148,14 +147,14 @@ class _TopOverlayRow extends StatelessWidget { final Function(EntryAction value) onActionSelected; const _TopOverlayRow({ - Key key, - @required this.quickActions, - @required this.inAppActions, - @required this.externalAppActions, - @required this.scale, - @required this.mainEntry, - @required this.pageEntry, - @required this.onActionSelected, + Key? key, + required this.quickActions, + required this.inAppActions, + required this.externalAppActions, + required this.scale, + required this.mainEntry, + required this.pageEntry, + required this.onActionSelected, }) : super(key: key); static const double padding = 8; @@ -166,21 +165,21 @@ class _TopOverlayRow extends StatelessWidget { children: [ OverlayButton( scale: scale, - child: Navigator.canPop(context) ? BackButton() : CloseButton(), + child: Navigator.canPop(context) ? const BackButton() : const CloseButton(), ), - Spacer(), + const Spacer(), ...quickActions.map((action) => _buildOverlayButton(context, action)), OverlayButton( scale: scale, child: PopupMenuButton( - key: Key('entry-menu-button'), + key: const Key('entry-menu-button'), itemBuilder: (context) => [ ...inAppActions.map((action) => _buildPopupMenuItem(context, action)), if (pageEntry.canRotateAndFlip) _buildRotateAndFlipMenuItems(context), - PopupMenuDivider(), + const PopupMenuDivider(), ...externalAppActions.map((action) => _buildPopupMenuItem(context, action)), - if (kDebugMode) ...[ - PopupMenuDivider(), + if (!kReleaseMode) ...[ + const PopupMenuDivider(), _buildPopupMenuItem(context, EntryAction.debug), ] ], @@ -195,7 +194,7 @@ class _TopOverlayRow extends StatelessWidget { } Widget _buildOverlayButton(BuildContext context, EntryAction action) { - Widget child; + Widget? child; void onPressed() => onActionSelected(action); switch (action) { case EntryAction.toggleFavourite: @@ -229,17 +228,17 @@ class _TopOverlayRow extends StatelessWidget { } return child != null ? Padding( - padding: EdgeInsetsDirectional.only(end: padding), + padding: const EdgeInsetsDirectional.only(end: padding), child: OverlayButton( scale: scale, child: child, ), ) - : SizedBox.shrink(); + : const SizedBox.shrink(); } PopupMenuEntry _buildPopupMenuItem(BuildContext context, EntryAction action) { - Widget child; + Widget? child; switch (action) { // in app actions case EntryAction.toggleFavourite: @@ -276,7 +275,7 @@ class _TopOverlayRow extends StatelessWidget { } PopupMenuItem _buildRotateAndFlipMenuItems(BuildContext context) { - Widget buildDivider() => SizedBox( + Widget buildDivider() => const SizedBox( height: 16, child: VerticalDivider( width: 1, @@ -313,10 +312,10 @@ class _TopOverlayRow extends StatelessWidget { class _FavouriteToggler extends StatefulWidget { final AvesEntry entry; final bool isMenuItem; - final VoidCallback onPressed; + final VoidCallback? onPressed; const _FavouriteToggler({ - @required this.entry, + required this.entry, this.isMenuItem = false, this.onPressed, }); @@ -326,7 +325,7 @@ class _FavouriteToggler extends StatefulWidget { } class _FavouriteTogglerState extends State<_FavouriteToggler> { - final ValueNotifier isFavouriteNotifier = ValueNotifier(null); + final ValueNotifier isFavouriteNotifier = ValueNotifier(false); @override void initState() { @@ -373,7 +372,7 @@ class _FavouriteTogglerState extends State<_FavouriteToggler> { ), Sweeper( key: ValueKey(widget.entry), - builder: (context) => Icon(AIcons.favourite, color: Colors.redAccent), + builder: (context) => const Icon(AIcons.favourite, color: Colors.redAccent), toggledNotifier: isFavouriteNotifier, ), ], diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index 9672a4ccc..8df109ade 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -20,8 +20,8 @@ class PanoramaPage extends StatefulWidget { final PanoramaInfo info; const PanoramaPage({ - @required this.entry, - @required this.info, + required this.entry, + required this.info, }); @override @@ -40,7 +40,7 @@ class _PanoramaPageState extends State { void initState() { super.initState(); _overlayVisible.addListener(_onOverlayVisibleChange); - WidgetsBinding.instance.addPostFrameCallback((_) => _initOverlay()); + WidgetsBinding.instance!.addPostFrameCallback((_) => _initOverlay()); } @override @@ -65,11 +65,11 @@ class _PanoramaPageState extends State { builder: (context, sensorControl, child) { return Panorama( sensorControl: sensorControl, - croppedArea: info.hasCroppedArea ? info.croppedAreaRect : Rect.fromLTWH(0.0, 0.0, 1.0, 1.0), - croppedFullWidth: info.hasCroppedArea ? info.fullPanoSize.width : 1.0, - croppedFullHeight: info.hasCroppedArea ? info.fullPanoSize.height : 1.0, + croppedArea: info.hasCroppedArea ? info.croppedAreaRect! : const Rect.fromLTWH(0.0, 0.0, 1.0, 1.0), + croppedFullWidth: info.hasCroppedArea ? info.fullPanoSize!.width : 1.0, + croppedFullHeight: info.hasCroppedArea ? info.fullPanoSize!.height : 1.0, onTap: (longitude, latitude, tilt) => _overlayVisible.value = !_overlayVisible.value, - child: child, + child: child as Image?, ); }, child: Image( @@ -92,7 +92,7 @@ class _PanoramaPageState extends State { selector: (c, mq) => mq.viewPadding + mq.viewInsets, builder: (c, mqPadding, child) { return Padding( - padding: EdgeInsets.all(8) + EdgeInsets.only(right: mqPadding.right, bottom: mqPadding.bottom), + padding: const EdgeInsets.all(8) + EdgeInsets.only(right: mqPadding.right, bottom: mqPadding.bottom), child: OverlayButton( child: ValueListenableBuilder( valueListenable: _sensorControl, @@ -148,7 +148,7 @@ class _PanoramaPageState extends State { Future _initOverlay() async { // wait for MaterialPageRoute.transitionDuration // to show overlay after page animation is complete - await Future.delayed(ModalRoute.of(context).transitionDuration * timeDilation); + await Future.delayed(ModalRoute.of(context)!.transitionDuration * timeDilation); await _onOverlayVisibleChange(); } diff --git a/lib/widgets/viewer/printer.dart b/lib/widgets/viewer/printer.dart index da21d2174..ec640d9a3 100644 --- a/lib/widgets/viewer/printer.dart +++ b/lib/widgets/viewer/printer.dart @@ -33,7 +33,7 @@ class EntryPrinter with FeedbackMixin { Future> _buildPages(BuildContext context) async { final pages = []; - void _addPdfPage(pdf.Widget pdfChild) { + void _addPdfPage(pdf.Widget? pdfChild) { if (pdfChild == null) return; final displaySize = entry.displaySize; pages.add(pdf.Page( @@ -49,20 +49,22 @@ class EntryPrinter with FeedbackMixin { if (entry.isMultiPage && !entry.isMotionPhoto) { final multiPageInfo = await metadataService.getMultiPageInfo(entry); - final pageCount = multiPageInfo.pageCount; - if (pageCount > 1) { - final streamController = StreamController.broadcast(); - showOpReport( - context: context, - opStream: streamController.stream, - itemCount: pageCount, - ); - for (var page = 0; page < pageCount; page++) { - final pageEntry = multiPageInfo.getPageEntryByIndex(page); - _addPdfPage(await _buildPageImage(pageEntry)); - streamController.sink.add(pageEntry); + if (multiPageInfo != null) { + final pageCount = multiPageInfo.pageCount; + if (pageCount > 1) { + final streamController = StreamController.broadcast(); + showOpReport( + context: context, + opStream: streamController.stream, + itemCount: pageCount, + ); + for (var page = 0; page < pageCount; page++) { + final pageEntry = multiPageInfo.getPageEntryByIndex(page); + _addPdfPage(await _buildPageImage(pageEntry)); + streamController.sink.add(pageEntry); + } + await streamController.close(); } - await streamController.close(); } } if (pages.isEmpty) { @@ -71,10 +73,10 @@ class EntryPrinter with FeedbackMixin { return pages; } - Future _buildPageImage(AvesEntry entry) async { + Future _buildPageImage(AvesEntry entry) async { if (entry.isSvg) { final bytes = await imageFileService.getSvg(entry.uri, entry.mimeType); - if (bytes != null && bytes.isNotEmpty) { + if (bytes.isNotEmpty) { return pdf.SvgImage(svg: utf8.decode(bytes)); } } else { diff --git a/lib/widgets/viewer/source_viewer_page.dart b/lib/widgets/viewer/source_viewer_page.dart index 62b40b044..17be3a598 100644 --- a/lib/widgets/viewer/source_viewer_page.dart +++ b/lib/widgets/viewer/source_viewer_page.dart @@ -9,7 +9,7 @@ class SourceViewerPage extends StatefulWidget { final Future Function() loader; const SourceViewerPage({ - @required this.loader, + required this.loader, }); @override @@ -17,7 +17,7 @@ class SourceViewerPage extends StatefulWidget { } class _SourceViewerPageState extends State { - Future _loader; + late Future _loader; @override void initState() { @@ -36,21 +36,21 @@ class _SourceViewerPageState extends State { future: _loader, builder: (context, snapshot) { if (snapshot.hasError) return Text(snapshot.error.toString()); - if (!snapshot.hasData) return SizedBox.shrink(); + if (!snapshot.hasData) return const SizedBox.shrink(); - final source = snapshot.data; + final source = snapshot.data!; final highlightView = AvesHighlightView( source, language: 'xml', theme: darculaTheme, - padding: EdgeInsets.all(8), - textStyle: TextStyle( + padding: const EdgeInsets.all(8), + textStyle: const TextStyle( fontSize: 12, ), tabSize: 4, ); return Container( - constraints: BoxConstraints.expand(), + constraints: const BoxConstraints.expand(), child: Scrollbar( child: SingleChildScrollView( child: SingleChildScrollView( diff --git a/lib/widgets/viewer/video/conductor.dart b/lib/widgets/viewer/video/conductor.dart index 7a026aac5..0b46c9301 100644 --- a/lib/widgets/viewer/video/conductor.dart +++ b/lib/widgets/viewer/video/conductor.dart @@ -1,6 +1,8 @@ import 'package:aves/model/entry.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/video/fijkplayer.dart'; +import 'package:collection/collection.dart'; +// ignore: import_of_legacy_library_into_null_safe import 'package:fijkplayer/fijkplayer.dart'; class VideoConductor { @@ -13,7 +15,7 @@ class VideoConductor { } Future dispose() async { - await Future.forEach(_controllers, (controller) => controller.dispose()); + await Future.forEach(_controllers, (controller) => controller.dispose()); _controllers.clear(); } @@ -31,9 +33,9 @@ class VideoConductor { return controller; } - AvesVideoController getController(AvesEntry entry) { - return _controllers.firstWhere((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId, orElse: () => null); + AvesVideoController? getController(AvesEntry entry) { + return _controllers.firstWhereOrNull((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId); } - Future pauseAll() => Future.forEach(_controllers, (controller) => controller.pause()); + Future pauseAll() => Future.forEach(_controllers, (controller) => controller.pause()); } diff --git a/lib/widgets/viewer/video/controller.dart b/lib/widgets/viewer/video/controller.dart index 3b586908b..bdbfef65d 100644 --- a/lib/widgets/viewer/video/controller.dart +++ b/lib/widgets/viewer/video/controller.dart @@ -33,10 +33,12 @@ abstract class AvesVideoController { int get currentPosition; - double get progress => (currentPosition ?? 0).toDouble() / duration; + double get progress => currentPosition.toDouble() / duration; Stream get positionStream; + ValueNotifier get sarNotifier; + Widget buildPlayerWidget(BuildContext context); } diff --git a/lib/widgets/viewer/video/fijkplayer.dart b/lib/widgets/viewer/video/fijkplayer.dart index 11fc20ba6..41ff90954 100644 --- a/lib/widgets/viewer/video/fijkplayer.dart +++ b/lib/widgets/viewer/video/fijkplayer.dart @@ -8,23 +8,27 @@ import 'package:aves/model/video/keys.dart'; import 'package:aves/model/video/metadata.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:collection/collection.dart'; + +// ignore: import_of_legacy_library_into_null_safe import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:tuple/tuple.dart'; class IjkPlayerAvesVideoController extends AvesVideoController { - FijkPlayer _instance; + late FijkPlayer _instance; final List _subscriptions = []; final StreamController _valueStreamController = StreamController.broadcast(); final AChangeNotifier _completedNotifier = AChangeNotifier(); Offset _macroBlockCrop = Offset.zero; final List _streams = []; - final ValueNotifier _selectedVideoStream = ValueNotifier(null); - final ValueNotifier _selectedAudioStream = ValueNotifier(null); - final ValueNotifier _selectedTextStream = ValueNotifier(null); - final ValueNotifier> _sar = ValueNotifier(null); - Timer _initialPlayTimer; + final ValueNotifier _selectedVideoStream = ValueNotifier(null); + final ValueNotifier _selectedAudioStream = ValueNotifier(null); + final ValueNotifier _selectedTextStream = ValueNotifier(null); + Timer? _initialPlayTimer; + + @override + final ValueNotifier sarNotifier = ValueNotifier(1); Stream get _valueStream => _valueStreamController.stream; @@ -57,7 +61,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { } Future _init({int startMillis = 0}) async { - _sar.value = Tuple2(1, 1); + sarNotifier.value = 1; _applyOptions(startMillis); // calling `setDataSource()` with `autoPlay` starts as soon as possible, but often yields initial artifacts @@ -80,7 +84,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { // playing with HW acceleration seems to skip the last frames of some videos // so HW acceleration is always disabled for gif-like videos where the last frames may be significant - final hwAccelerationEnabled = settings.enableVideoHardwareAcceleration && entry.durationMillis > gifLikeVideoDurationThreshold.inMilliseconds; + final hwAccelerationEnabled = settings.enableVideoHardwareAcceleration && entry.durationMillis! > gifLikeVideoDurationThreshold.inMilliseconds; // TODO TLAD HW codecs sometimes fail when seek-starting some videos, e.g. MP2TS/h264(HDPR) if (hwAccelerationEnabled) { @@ -142,12 +146,12 @@ class IjkPlayerAvesVideoController extends AvesVideoController { } }); - StreamSummary _getSelectedStream(String selectedIndexKey) { + StreamSummary? _getSelectedStream(String selectedIndexKey) { final indexString = mediaInfo[selectedIndexKey]; if (indexString != null) { final index = int.tryParse(indexString); if (index != null && index != -1) { - return _streams.firstWhere((stream) => stream.index == index, orElse: () => null); + return _streams.firstWhereOrNull((stream) => stream.index == index); } } return null; @@ -158,12 +162,12 @@ class IjkPlayerAvesVideoController extends AvesVideoController { _selectedTextStream.value = _getSelectedStream(Keys.selectedTextStream); if (_selectedVideoStream.value != null) { - final streamIndex = _selectedVideoStream.value.index; - final streamInfo = allStreams.firstWhere((stream) => stream[Keys.index] == streamIndex, orElse: () => null); + final streamIndex = _selectedVideoStream.value!.index; + final streamInfo = allStreams.firstWhereOrNull((stream) => stream[Keys.index] == streamIndex); if (streamInfo != null) { - final num = streamInfo[Keys.sarNum]; - final den = streamInfo[Keys.sarDen]; - _sar.value = Tuple2((num ?? 0) != 0 ? num : 1, (den ?? 0) != 0 ? den : 1); + final num = streamInfo[Keys.sarNum] ?? 0; + final den = streamInfo[Keys.sarDen] ?? 0; + sarNotifier.value = (num != 0 ? num : 1) / (den != 0 ? den : 1); } } } @@ -219,7 +223,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { int get duration { final controllerDuration = _instance.value.duration.inMilliseconds; // use expected duration when controller duration is not set yet - return (controllerDuration == null || controllerDuration == 0) ? entry.durationMillis : controllerDuration; + return (controllerDuration == 0) ? entry.durationMillis! : controllerDuration; } @override @@ -230,15 +234,12 @@ class IjkPlayerAvesVideoController extends AvesVideoController { @override Widget buildPlayerWidget(BuildContext context) { - return ValueListenableBuilder>( - valueListenable: _sar, + return ValueListenableBuilder( + valueListenable: sarNotifier, builder: (context, sar, child) { - final sarNum = sar.item1; - final sarDen = sar.item2; // derive DAR (Display Aspect Ratio) from SAR (Storage Aspect Ratio), if any // e.g. 960x536 (~16:9) with SAR 4:3 should be displayed as ~2.39:1 - final dar = entry.displayAspectRatio * sarNum / sarDen; - // TODO TLAD notify SAR to make the magnifier and minimap use the rendering DAR instead of entry DAR + final dar = entry.displayAspectRatio * sar; return FijkView( player: _instance, fit: FijkFit( @@ -247,7 +248,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { alignment: _alignmentForRotation(entry.rotationDegrees), macroBlockCrop: _macroBlockCrop, ), - panelBuilder: (player, data, context, viewSize, texturePos) => SizedBox(), + panelBuilder: (player, data, context, viewSize, texturePos) => const SizedBox(), color: Colors.transparent, ); }); @@ -288,7 +289,6 @@ extension ExtraIjkStatus on FijkState { case FijkState.error: return VideoStatus.error; } - return VideoStatus.idle; } } @@ -321,7 +321,7 @@ extension ExtraFijkPlayer on FijkPlayer { enum StreamType { video, audio, text } extension ExtraStreamType on StreamType { - static StreamType fromTypeString(String type) { + static StreamType? fromTypeString(String? type) { switch (type) { case StreamTypes.video: return StreamType.video; @@ -338,14 +338,14 @@ extension ExtraStreamType on StreamType { class StreamSummary { final StreamType type; - final int index; - final String language, title; + final int? index; + final String? language, title; const StreamSummary({ - @required this.type, - @required this.index, - @required this.language, - @required this.title, + required this.type, + required this.index, + required this.language, + required this.title, }); @override diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 92f960cea..9a2e2091b 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -2,12 +2,11 @@ import 'dart:async'; import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:aves/model/entry.dart'; -import 'package:aves/model/entry_images.dart'; import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/collection/thumbnail/raster.dart'; import 'package:aves/widgets/common/magnifier/controller/controller.dart'; import 'package:aves/widgets/common/magnifier/controller/state.dart'; import 'package:aves/widgets/common/magnifier/magnifier.dart'; @@ -30,15 +29,15 @@ import 'package:provider/provider.dart'; class EntryPageView extends StatefulWidget { final AvesEntry mainEntry, pageEntry; - final Size viewportSize; - final VoidCallback onDisposed; + final Size? viewportSize; + final VoidCallback? onDisposed; static const decorationCheckSize = 20.0; const EntryPageView({ - Key key, - this.mainEntry, - this.pageEntry, + Key? key, + required this.mainEntry, + required this.pageEntry, this.viewportSize, this.onDisposed, }) : super(key: key); @@ -48,7 +47,7 @@ class EntryPageView extends StatefulWidget { } class _EntryPageViewState extends State { - MagnifierController _magnifierController; + late MagnifierController _magnifierController; final ValueNotifier _viewStateNotifier = ValueNotifier(ViewState.zero); final List _subscriptions = []; @@ -56,7 +55,7 @@ class _EntryPageViewState extends State { AvesEntry get entry => widget.pageEntry; - Size get viewportSize => widget.viewportSize; + Size? get viewportSize => widget.viewportSize; static const initialScale = ScaleLevel(ref: ScaleReference.contained); static const minScale = ScaleLevel(ref: ScaleReference.contained); @@ -96,7 +95,7 @@ class _EntryPageViewState extends State { minScale: minScale, maxScale: maxScale, initialScale: initialScale, - viewportSize: viewportSize, + viewportSize: viewportSize!, childSize: entry.displaySize, ).initialScale, viewportSize, @@ -109,7 +108,7 @@ class _EntryPageViewState extends State { } void _unregisterWidget() { - _magnifierController?.dispose(); + _magnifierController.dispose(); _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); @@ -120,7 +119,7 @@ class _EntryPageViewState extends State { final child = AnimatedBuilder( animation: entry.imageChangeNotifier, builder: (context, child) { - Widget child; + Widget? child; if (entry.isSvg) { child = _buildSvgView(); } else if (!entry.displaySize.isEmpty) { @@ -138,11 +137,11 @@ class _EntryPageViewState extends State { }, ); - return Consumer( + return Consumer( builder: (context, info, child) => Hero( - tag: info?.entry == mainEntry ? hashValues(info.collectionId, mainEntry) : hashCode, + tag: info != null && info.entry == mainEntry ? hashValues(info.collectionId, mainEntry) : hashCode, transitionOnUserGestures: true, - child: child, + child: child!, ), child: child, ); @@ -167,7 +166,7 @@ class _EntryPageViewState extends State { final colorFilter = background.isColor ? ColorFilter.mode(background.color, BlendMode.dstOver) : null; var child = _buildMagnifier( - maxScale: ScaleLevel(factor: double.infinity), + maxScale: const ScaleLevel(factor: double.infinity), scaleStateCycle: _vectorScaleStateCycle, child: SvgPicture( UriPicture( @@ -190,16 +189,21 @@ class _EntryPageViewState extends State { Widget _buildVideoView() { final videoController = context.read().getController(entry); - if (videoController == null) return SizedBox(); + if (videoController == null) return const SizedBox(); return Stack( fit: StackFit.expand, children: [ - _buildMagnifier( - child: VideoView( - entry: entry, - controller: videoController, - ), - ), + ValueListenableBuilder( + valueListenable: videoController.sarNotifier, + builder: (context, sar, child) { + return _buildMagnifier( + displaySize: entry.videoDisplaySize(sar), + child: VideoView( + entry: entry, + controller: videoController, + ), + ); + }), // fade out image to ease transition with the player StreamBuilder( stream: videoController.statusStream, @@ -213,9 +217,11 @@ class _EntryPageViewState extends State { duration: Durations.viewerVideoPlayerTransition, child: GestureDetector( onTap: _onTap, - child: Image( - image: entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName)), + child: RasterImageThumbnail( + entry: entry, + extent: context.select((mq) => mq.size.shortestSide), fit: BoxFit.contain, + showLoadingBackground: false, ), ), ), @@ -230,13 +236,14 @@ class _EntryPageViewState extends State { ScaleLevel maxScale = maxScale, ScaleStateCycle scaleStateCycle = defaultScaleStateCycle, bool applyScale = true, - @required Widget child, + Size? displaySize, + required Widget child, }) { return Magnifier( // key includes modified date to refresh when the image is modified by metadata (e.g. rotated) key: ValueKey('${entry.pageId}_${entry.dateModifiedSecs}'), controller: _magnifierController, - childSize: entry.displaySize, + childSize: displaySize ?? entry.displaySize, minScale: minScale, maxScale: maxScale, initialScale: initialScale, diff --git a/lib/widgets/viewer/visual/error.dart b/lib/widgets/viewer/visual/error.dart index 1cbcc81ce..9d72b010e 100644 --- a/lib/widgets/viewer/visual/error.dart +++ b/lib/widgets/viewer/visual/error.dart @@ -12,8 +12,8 @@ class ErrorView extends StatefulWidget { final VoidCallback onTap; const ErrorView({ - @required this.entry, - @required this.onTap, + required this.entry, + required this.onTap, }); @override @@ -21,20 +21,20 @@ class ErrorView extends StatefulWidget { } class _ErrorViewState extends State { - Future _exists; + late Future _exists; AvesEntry get entry => widget.entry; @override void initState() { super.initState(); - _exists = entry.path != null ? File(entry.path).exists() : SynchronousFuture(true); + _exists = entry.path != null ? File(entry.path!).exists() : SynchronousFuture(true); } @override Widget build(BuildContext context) { return GestureDetector( - onTap: () => widget.onTap?.call(), + onTap: () => widget.onTap(), // use container to expand constraints, so that the user can tap anywhere child: Container( // opaque to cover potential lower quality layer below @@ -42,8 +42,8 @@ class _ErrorViewState extends State { child: FutureBuilder( future: _exists, builder: (context, snapshot) { - if (snapshot.connectionState != ConnectionState.done) return SizedBox(); - final exists = snapshot.data; + if (snapshot.connectionState != ConnectionState.done) return const SizedBox(); + final exists = snapshot.data!; return EmptyContent( icon: exists ? AIcons.error : AIcons.broken, text: exists ? context.l10n.viewerErrorUnknown : context.l10n.viewerErrorDoesNotExist, diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index 77bf923b5..e246eaa4c 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -7,7 +7,6 @@ import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/utils/math_utils.dart'; -import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:aves/widgets/viewer/visual/state.dart'; @@ -21,9 +20,9 @@ class RasterImageView extends StatefulWidget { final ImageErrorWidgetBuilder errorBuilder; const RasterImageView({ - @required this.entry, - @required this.viewStateNotifier, - @required this.errorBuilder, + required this.entry, + required this.viewStateNotifier, + required this.errorBuilder, }); @override @@ -31,13 +30,13 @@ class RasterImageView extends StatefulWidget { } class _RasterImageViewState extends State { - Size _displaySize; + late Size _displaySize; bool _isTilingInitialized = false; - int _maxSampleSize; - double _tileSide; - Matrix4 _tileTransform; - ImageStream _fullImageStream; - ImageStreamListener _fullImageListener; + late int _maxSampleSize; + late double _tileSide; + Matrix4? _tileTransform; + ImageStream? _fullImageStream; + late ImageStreamListener _fullImageListener; final ValueNotifier _fullImageLoaded = ValueNotifier(false); AvesEntry get entry => widget.entry; @@ -48,7 +47,7 @@ class _RasterImageViewState extends State { ViewState get viewState => viewStateNotifier.value; - ImageProvider get thumbnailProvider => entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName)); + ImageProvider get thumbnailProvider => entry.bestCachedThumbnail; ImageProvider get fullImageProvider { if (entry.useTiles) { @@ -91,7 +90,7 @@ class _RasterImageViewState extends State { void _registerFullImage() { _fullImageStream = fullImageProvider.resolve(ImageConfiguration.empty); - _fullImageStream.addListener(_fullImageListener); + _fullImageStream!.addListener(_fullImageListener); } void _unregisterFullImage() { @@ -106,18 +105,16 @@ class _RasterImageViewState extends State { @override Widget build(BuildContext context) { - if (viewStateNotifier == null) return SizedBox.shrink(); - final useTiles = entry.useTiles; return ValueListenableBuilder( valueListenable: viewStateNotifier, builder: (context, viewState, child) { final viewportSize = viewState.viewportSize; final viewportSized = viewportSize?.isEmpty == false; - if (viewportSized && useTiles && !_isTilingInitialized) _initTiling(viewportSize); + if (viewportSized && useTiles && !_isTilingInitialized) _initTiling(viewportSize!); return SizedBox.fromSize( - size: _displaySize * viewState.scale, + size: _displaySize * viewState.scale!, child: Stack( alignment: Alignment.center, children: [ @@ -129,7 +126,7 @@ class _RasterImageViewState extends State { image: fullImageProvider, gaplessPlayback: true, errorBuilder: widget.errorBuilder, - width: (_displaySize * viewState.scale).width, + width: (_displaySize * viewState.scale!).width, fit: BoxFit.contain, filterQuality: FilterQuality.medium, ), @@ -161,10 +158,10 @@ class _RasterImageViewState extends State { } Widget _buildLoading() { - return ValueListenableBuilder( + return ValueListenableBuilder( valueListenable: _fullImageLoaded, builder: (context, fullImageLoaded, child) { - if (fullImageLoaded) return SizedBox.shrink(); + if (fullImageLoaded) return const SizedBox.shrink(); return Center( child: AspectRatio( @@ -181,13 +178,11 @@ class _RasterImageViewState extends State { } Widget _buildBackground() { - final viewportSize = viewState.viewportSize; - assert(viewportSize != null); - - final viewSize = _displaySize * viewState.scale; + final viewportSize = viewState.viewportSize!; + final viewSize = _displaySize * viewState.scale!; final decorationOffset = ((viewSize - viewportSize) as Offset) / 2 - viewState.position; // deflate as a quick way to prevent background bleed - final decorationSize = (applyBoxFit(BoxFit.none, viewSize, viewportSize).source - Offset(.5, .5)) as Size; + final decorationSize = (applyBoxFit(BoxFit.none, viewSize, viewportSize).source - const Offset(.5, .5)) as Size; Widget child; final background = settings.rasterBackground; @@ -195,10 +190,10 @@ class _RasterImageViewState extends State { final side = viewportSize.shortestSide; final checkSize = side / ((side / EntryPageView.decorationCheckSize).round()); final offset = ((decorationSize - viewportSize) as Offset) / 2; - child = ValueListenableBuilder( + child = ValueListenableBuilder( valueListenable: _fullImageLoaded, builder: (context, fullImageLoaded, child) { - if (!fullImageLoaded) return SizedBox.shrink(); + if (!fullImageLoaded) return const SizedBox.shrink(); return CustomPaint( painter: CheckeredPainter( @@ -230,7 +225,7 @@ class _RasterImageViewState extends State { final displayWidth = _displaySize.width.round(); final displayHeight = _displaySize.height.round(); final viewRect = _getViewRect(displayWidth, displayHeight); - final scale = viewState.scale; + final scale = viewState.scale!; // for the largest sample size (matching the initial scale), the whole image is in view // so we subsample the whole image without tiling @@ -271,9 +266,9 @@ class _RasterImageViewState extends State { } Rect _getViewRect(int displayWidth, int displayHeight) { - final scale = viewState.scale; + final scale = viewState.scale!; final centerOffset = viewState.position; - final viewportSize = viewState.viewportSize; + final viewportSize = viewState.viewportSize!; final viewOrigin = Offset( ((displayWidth * scale - viewportSize.width) / 2 - centerOffset.dx), ((displayHeight * scale - viewportSize.height) / 2 - centerOffset.dy), @@ -281,14 +276,14 @@ class _RasterImageViewState extends State { return viewOrigin & viewportSize; } - Tuple2> _getTileRects({ - @required int x, - @required int y, - @required int regionSide, - @required int displayWidth, - @required int displayHeight, - @required double scale, - @required Rect viewRect, + Tuple2>? _getTileRects({ + required int x, + required int y, + required int regionSide, + required int displayWidth, + required int displayHeight, + required double scale, + required Rect viewRect, }) { final nextX = x + regionSide; final nextY = y + regionSide; @@ -303,8 +298,8 @@ class _RasterImageViewState extends State { if (_tileTransform != null) { // apply EXIF orientation final regionRectDouble = Rect.fromLTWH(x.toDouble(), y.toDouble(), thisRegionWidth.toDouble(), thisRegionHeight.toDouble()); - final tl = MatrixUtils.transformPoint(_tileTransform, regionRectDouble.topLeft); - final br = MatrixUtils.transformPoint(_tileTransform, regionRectDouble.bottomRight); + final tl = MatrixUtils.transformPoint(_tileTransform!, regionRectDouble.topLeft); + final br = MatrixUtils.transformPoint(_tileTransform!, regionRectDouble.bottomRight); regionRect = Rectangle.fromPoints( Point(tl.dx.round(), tl.dy.round()), Point(br.dx.round(), br.dy.round()), @@ -330,14 +325,14 @@ class RegionTile extends StatefulWidget { // `tileRect` uses Flutter view coordinates // `regionRect` uses the raw image pixel coordinates final Rect tileRect; - final Rectangle regionRect; + final Rectangle? regionRect; final int sampleSize; const RegionTile({ - @required this.entry, - @required this.tileRect, + required this.entry, + required this.tileRect, this.regionRect, - @required this.sampleSize, + required this.sampleSize, }); @override @@ -354,7 +349,7 @@ class RegionTile extends StatefulWidget { } class _RegionTileState extends State { - RegionProvider _provider; + late RegionProvider _provider; AvesEntry get entry => widget.entry; @@ -388,15 +383,13 @@ class _RegionTileState extends State { } void _initProvider() { - if (!entry.canDecode) return; - _provider = entry.getRegion( sampleSize: widget.sampleSize, region: widget.regionRect, ); } - void _pauseProvider() => _provider?.pause(); + void _pauseProvider() => _provider.pause(); @override Widget build(BuildContext context) { diff --git a/lib/widgets/viewer/visual/state.dart b/lib/widgets/viewer/visual/state.dart index 97760372f..2bcc3d3a4 100644 --- a/lib/widgets/viewer/visual/state.dart +++ b/lib/widgets/viewer/visual/state.dart @@ -3,8 +3,8 @@ import 'package:flutter/widgets.dart'; class ViewState { final Offset position; - final double scale; - final Size viewportSize; + final double? scale; + final Size? viewportSize; static const ViewState zero = ViewState(Offset.zero, 0, null); diff --git a/lib/widgets/viewer/visual/vector.dart b/lib/widgets/viewer/visual/vector.dart index 1fe9d3e28..da5cfa639 100644 --- a/lib/widgets/viewer/visual/vector.dart +++ b/lib/widgets/viewer/visual/vector.dart @@ -10,9 +10,9 @@ class VectorViewCheckeredBackground extends StatelessWidget { final Widget child; const VectorViewCheckeredBackground({ - @required this.displaySize, - @required this.viewStateNotifier, - @required this.child, + required this.displaySize, + required this.viewStateNotifier, + required this.child, }); @override @@ -21,12 +21,12 @@ class VectorViewCheckeredBackground extends StatelessWidget { valueListenable: viewStateNotifier, builder: (context, viewState, child) { final viewportSize = viewState.viewportSize; - if (viewportSize == null) return child; + if (viewportSize == null) return child!; final side = viewportSize.shortestSide; final checkSize = side / ((side / EntryPageView.decorationCheckSize).round()); - final viewSize = displaySize * viewState.scale; + final viewSize = displaySize * viewState.scale!; final decorationSize = applyBoxFit(BoxFit.none, viewSize, viewportSize).source; final offset = ((decorationSize - viewportSize) as Offset) / 2; @@ -43,7 +43,7 @@ class VectorViewCheckeredBackground extends StatelessWidget { ), ), ), - child, + child!, ], ); }, diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video.dart index eda8c6d2a..ec17a12e4 100644 --- a/lib/widgets/viewer/visual/video.dart +++ b/lib/widgets/viewer/visual/video.dart @@ -7,9 +7,9 @@ class VideoView extends StatefulWidget { final AvesVideoController controller; const VideoView({ - Key key, - @required this.entry, - @required this.controller, + Key? key, + required this.entry, + required this.controller, }) : super(key: key); @override @@ -50,11 +50,10 @@ class _VideoViewState extends State { @override Widget build(BuildContext context) { - if (controller == null) return SizedBox(); return StreamBuilder( stream: controller.statusStream, builder: (context, snapshot) { - return controller.isReady ? controller.buildPlayerWidget(context) : SizedBox(); + return controller.isReady ? controller.buildPlayerWidget(context) : const SizedBox(); }); } diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index 5b8fe9181..249a235eb 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -22,7 +22,7 @@ class WelcomePage extends StatefulWidget { class _WelcomePageState extends State { bool _hasAcceptedTerms = false; - Future _termsLoader; + late Future _termsLoader; @override void initState() { @@ -37,12 +37,12 @@ class _WelcomePageState extends State { body: SafeArea( child: Container( alignment: Alignment.center, - padding: EdgeInsets.all(16.0), + padding: const EdgeInsets.all(16.0), child: FutureBuilder( future: _termsLoader, builder: (context, snapshot) { - if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); - final terms = snapshot.data; + if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); + final terms = snapshot.data!; return Column( mainAxisSize: MainAxisSize.min, children: _toStaggeredList( @@ -57,7 +57,7 @@ class _WelcomePageState extends State { children: [ ..._buildTop(context), Flexible(child: _buildTerms(terms)), - SizedBox(height: 16), + const SizedBox(height: 16), ..._buildBottomControls(context), ], ), @@ -78,21 +78,21 @@ class _WelcomePageState extends State { return [ ...(context.select((mq) => mq.orientation) == Orientation.portrait ? [ - AvesLogo(size: 64), - SizedBox(height: 16), + const AvesLogo(size: 64), + const SizedBox(height: 16), message, ] : [ Row( mainAxisSize: MainAxisSize.min, children: [ - AvesLogo(size: 48), - SizedBox(width: 16), + const AvesLogo(size: 48), + const SizedBox(width: 16), message, ], ) ]), - SizedBox(height: 16), + const SizedBox(height: 16), ]; } @@ -102,28 +102,32 @@ class _WelcomePageState extends State { children: [ LabeledCheckbox( value: settings.isCrashlyticsEnabled, - onChanged: (v) => setState(() => settings.isCrashlyticsEnabled = v), + onChanged: (v) { + if (v != null) setState(() => settings.isCrashlyticsEnabled = v); + }, text: context.l10n.welcomeAnalyticsToggle, ), LabeledCheckbox( - key: Key('agree-checkbox'), + key: const Key('agree-checkbox'), value: _hasAcceptedTerms, - onChanged: (v) => setState(() => _hasAcceptedTerms = v), + onChanged: (v) { + if (v != null) setState(() => _hasAcceptedTerms = v); + }, text: context.l10n.welcomeTermsToggle, ), ], ); final button = ElevatedButton( - key: Key('continue-button'), + key: const Key('continue-button'), onPressed: _hasAcceptedTerms ? () { settings.hasAcceptedTerms = true; Navigator.pushReplacement( context, MaterialPageRoute( - settings: RouteSettings(name: HomePage.routeName), - builder: (context) => HomePage(), + settings: const RouteSettings(name: HomePage.routeName), + builder: (context) => const HomePage(), ), ); } @@ -141,7 +145,7 @@ class _WelcomePageState extends State { crossAxisAlignment: CrossAxisAlignment.end, children: [ checkboxes, - Spacer(), + const Spacer(), button, ], ), @@ -150,16 +154,16 @@ class _WelcomePageState extends State { Widget _buildTerms(String terms) { return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(16)), color: Colors.white10, ), - constraints: BoxConstraints(maxWidth: 460), + constraints: const BoxConstraints(maxWidth: 460), child: ClipRRect( - borderRadius: BorderRadius.circular(16), + borderRadius: const BorderRadius.all(Radius.circular(16)), child: Theme( data: Theme.of(context).copyWith( - scrollbarTheme: ScrollbarThemeData( + scrollbarTheme: const ScrollbarThemeData( radius: Radius.circular(16), crossAxisMargin: 6, mainAxisMargin: 16, @@ -171,7 +175,7 @@ class _WelcomePageState extends State { data: terms, selectable: true, onTapLink: (text, href, title) async { - if (await canLaunch(href)) { + if (href != null && await canLaunch(href)) { await launch(href); } }, @@ -186,10 +190,10 @@ class _WelcomePageState extends State { // as of flutter_staggered_animations v0.1.2, `AnimationConfiguration.toStaggeredList` does not handle `Flexible` widgets // so we use this workaround instead static List _toStaggeredList({ - Duration duration, - Duration delay, - @required Widget Function(Widget) childAnimationBuilder, - @required List children, + required Duration duration, + required Duration delay, + required Widget Function(Widget) childAnimationBuilder, + required List children, }) => children .asMap() diff --git a/pubspec.lock b/pubspec.lock index b264a4827..e46cbdebd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "21.0.0" + version: "22.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.7.1" archive: dependency: transitive description: @@ -28,14 +28,14 @@ packages: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.6.1" barcode: dependency: transitive description: @@ -99,34 +99,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" - connectivity: + connectivity_plus: dependency: "direct main" description: - name: connectivity + name: connectivity_plus url: "https://pub.dartlang.org" source: hosted - version: "3.0.3" - connectivity_for_web: + version: "1.0.1" + connectivity_plus_linux: dependency: transitive description: - name: connectivity_for_web + name: connectivity_plus_linux url: "https://pub.dartlang.org" source: hosted - version: "0.4.0" - connectivity_macos: + version: "1.0.1" + connectivity_plus_macos: dependency: transitive description: - name: connectivity_macos + name: connectivity_plus_macos url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" - connectivity_platform_interface: + version: "1.0.1" + connectivity_plus_platform_interface: dependency: transitive description: - name: connectivity_platform_interface + name: connectivity_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "1.0.1" + connectivity_plus_web: + dependency: transitive + description: + name: connectivity_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + connectivity_plus_windows: + dependency: transitive + description: + name: connectivity_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" convert: dependency: transitive description: @@ -147,14 +161,21 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" + dbus: + dependency: transitive + description: + name: dbus + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.5" decorated_icon: dependency: "direct main" description: @@ -191,16 +212,16 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.2" fijkplayer: dependency: "direct main" description: path: "." ref: aves - resolved-ref: "7bb26e681ceef9d7b311f0b0f51eab99c3590474" + resolved-ref: "33aaf201bf761a50755971ba714c80693717b9f9" url: "git://github.com/deckerst/fijkplayer.git" source: git - version: "0.8.7" + version: "0.9.0" file: dependency: transitive description: @@ -221,56 +242,56 @@ packages: name: firebase_analytics url: "https://pub.dartlang.org" source: hosted - version: "8.0.2" + version: "8.1.1" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.3.0+1" firebase_core: dependency: "direct main" description: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.1" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.0.1" firebase_core_web: dependency: transitive description: name: firebase_core_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.1.0" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.5" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.0.5" flutter: dependency: "direct main" description: flutter @@ -295,13 +316,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.7.0" - flutter_image: - dependency: transitive - description: - name: flutter_image - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -310,26 +324,24 @@ packages: flutter_map: dependency: "direct main" description: - path: "." - ref: "issues/829-nullsafety" - resolved-ref: "622d7e894034681c46470a4e4c94964d0ccd6454" - url: "git://github.com/fleaflet/flutter_map.git" - source: git - version: "0.12.0" + name: flutter_map + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.0" flutter_markdown: dependency: "direct main" description: name: flutter_markdown url: "https://pub.dartlang.org" source: hosted - version: "0.6.1" + version: "0.6.2" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" flutter_staggered_animations: dependency: "direct main" description: @@ -365,14 +377,14 @@ packages: name: get_it url: "https://pub.dartlang.org" source: hosted - version: "6.1.1" + version: "7.1.3" github: dependency: "direct main" description: name: github url: "https://pub.dartlang.org" source: hosted - version: "8.1.0" + version: "8.1.1" glob: dependency: transitive description: @@ -393,7 +405,7 @@ packages: name: google_maps_flutter url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.6" google_maps_flutter_platform_interface: dependency: transitive description: @@ -414,7 +426,7 @@ packages: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.13.1" + version: "0.13.3" http_multi_server: dependency: transitive description: @@ -562,13 +574,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" - package_info: + package_info_plus: dependency: "direct main" description: - name: package_info + name: package_info_plus url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "1.0.1" + package_info_plus_linux: + dependency: transitive + description: + name: package_info_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + package_info_plus_macos: + dependency: transitive + description: + name: package_info_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + package_info_plus_web: + dependency: transitive + description: + name: package_info_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + package_info_plus_windows: + dependency: transitive + description: + name: package_info_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" palette_generator: dependency: "direct main" description: @@ -596,14 +643,14 @@ packages: name: path_drawing url: "https://pub.dartlang.org" source: hosted - version: "0.5.0" + version: "0.5.1" path_parsing: dependency: transitive description: name: path_parsing url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.2.1" path_provider_linux: dependency: transitive description: @@ -652,14 +699,14 @@ packages: name: permission_handler url: "https://pub.dartlang.org" source: hosted - version: "7.0.0" + version: "8.0.1" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "3.5.1" petitparser: dependency: transitive description: @@ -750,7 +797,7 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" shared_preferences_linux: dependency: transitive description: @@ -792,7 +839,7 @@ packages: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.4" shelf_packages_handler: dependency: transitive description: @@ -878,9 +925,11 @@ packages: streams_channel: dependency: "direct main" description: - name: streams_channel - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: HEAD + resolved-ref: d644fedd9cb79a45b1b92788880e81b846a69d9b + url: "git://github.com/deckerst/aves_streams_channel.git" + source: git version: "0.3.0" string_scanner: dependency: transitive @@ -965,7 +1014,7 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.3" + version: "6.0.6" url_launcher_linux: dependency: transitive description: @@ -986,14 +1035,14 @@ packages: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" url_launcher_windows: dependency: transitive description: @@ -1021,7 +1070,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "6.1.0+1" + version: "6.2.0" watcher: dependency: transitive description: @@ -1035,7 +1084,7 @@ packages: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" webdriver: dependency: transitive description: @@ -1056,7 +1105,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.4" wkt_parser: dependency: transitive description: @@ -1077,7 +1126,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.1.0" + version: "5.1.2" yaml: dependency: transitive description: @@ -1086,5 +1135,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.13.0 <3.0.0" flutter: ">=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 51670fcd7..af2ebc7d4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,23 +1,14 @@ name: aves description: A visual media gallery and metadata explorer app. repository: https://github.com/deckerst/aves -version: 1.4.1+45 +version: 1.4.2+46 publish_to: none environment: - sdk: '>=2.10.0 <3.0.0' + sdk: '>=2.12.0 <3.0.0' -# TODO TLAD switch to Flutter stable when possible, currently on dev/beta because of the following mess: -# printing >=5.0.1 depends on pdf ^3.0.1, pdf >=3.0.1 depends on crypto ^3.0.0 and archive ^3.1.0 -# but `flutter_driver` (shipped with Flutter) dependencies are too old in stable v2.0.1 -# bump `crypto` and others - 2021/02/05 https://github.com/flutter/flutter/commit/bc1cf4945841ba5874f5262b8146d52750e7c11f -# bump `archive` from 3.0.0 to 3.1.2 - 2021/03/04 https://github.com/flutter/flutter/commit/ddcb8d7d6d3fcedc906b2f1bf26b73c018d3dc28 - -# not null safe, as of 2021/04/28 +# not null safe, as of 2021/06/07 # `charts_flutter` - https://github.com/google/charts/issues/579 -# `fijkplayer` - https://github.com/befovy/fijkplayer/issues/381 -# `flutter_map` - https://github.com/fleaflet/flutter_map/issues/829 -# `streams_channel` - unmaintained? - no issue/PR dependencies: flutter: @@ -26,7 +17,7 @@ dependencies: sdk: flutter charts_flutter: collection: - connectivity: + connectivity_plus: country_code: decorated_icon: event_bus: @@ -42,9 +33,6 @@ dependencies: firebase_crashlytics: flutter_highlight: flutter_map: - git: - url: git://github.com/fleaflet/flutter_map.git - ref: issues/829-nullsafety flutter_markdown: flutter_staggered_animations: flutter_svg: @@ -56,7 +44,7 @@ dependencies: latlong2: material_design_icons_flutter: overlay_support: - package_info: + package_info_plus: palette_generator: # TODO TLAD upgrade panorama when this is fixed: https://github.com/zesage/panorama/issues/25 (bug in v0.4.1) panorama: 0.4.0 @@ -69,6 +57,8 @@ dependencies: shared_preferences: sqflite: streams_channel: + git: + url: git://github.com/deckerst/aves_streams_channel.git tuple: url_launcher: version: diff --git a/shaders_2.2.0-10.1.pre.sksl.json b/shaders_2.2.0-10.1.pre.sksl.json deleted file mode 100644 index 2c591b909..000000000 --- a/shaders_2.2.0-10.1.pre.sksl.json +++ /dev/null @@ -1 +0,0 @@ -{"platform":"android","name":"SM G970N","engineRevision":"d2a2e93510ad6cfc3d62a90d903b7056e4da8264","data":{"K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQYAAAAAAACAQAAAAGJCABAAAAAICAAAAABAGOAACAQAAAABQCVAEQAEAIAAAAAAAAAVAAAAAAAAQAAAABAMQCAAAAA":"BAAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAcFAAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs3XTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMF9jMDsKdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMF9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwX2MwX2MwLnksIHVjbGFtcF9TdGFnZTFfYzBfYzBfYzBfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzBfYzAoX2lucHV0LCAoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwX2MwKSAqIF9jb29yZHMueHkxKS54eSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgXzJfY29sb3IgPSBoYWxmNCgwLjApOwoJZmxvYXQyIF8zX2Nvb3JkID0gKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCAtIGZsb2F0MigoMTIuMCAqIHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNl0ueCkpOwoJcmV0dXJuIF8yX2NvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fU3RhZ2UxX2MwX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQCAACAAAAA5BIABAAAAAAAAJQDBAMAACAAAAAAAAKAAQAAAABAAAAACAJAEIAAAAA":"BAAAAExTS1OjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMF9jMCkgKiAodW1hdHJpeF9TdGFnZTFfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAgAQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gc3Vic2V0Q29vcmQueTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AWQAGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAIAGCIDYAAAQAAAAAAAAAVAAAABAAAAAAABBEMAAAA":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQDyAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTFfYzAuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxX2MwLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","K4QAAAAAAAGAAAAAAIWP57ZDH37P7777777QCAAAAAAAAAAAMIQHSAAAAAAQAAAAABKAAAAAAABAAAAACAZAEAAAAA":"BAAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBABEDAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTFfYzAuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxX2MwLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","DIQAAAAAMAAAAABAYAROFA2CAIAAAABAAAAAAAAAAAAAAVAAAAAAAAQAAAABAMQC":"BAAAAExTS1NQAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBmbG9hdCB2VGV4SW5kZXhfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNTaXplSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSBmbG9hdCh0ZXhJZHgpOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChpblBvc2l0aW9uLnggLCBpblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAPkBAAB1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1N0YWdlMDsKaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB2VGV4dHVyZUNvb3Jkc19TdGFnZTApLnJycnI7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","K4JAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAABAAAAAABBAMACAAUAAAABAAAAAAABBEMAAA":"BAAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAA2wEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdGV4Q29vcmQpICogaGFsZjQoMSkpKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","KYMAAAAABCYIR6AYYAAAAAAAAAAAAAAACUAAAAEAAAAAAAEERQAA":"BAAAAExTS1NwAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkRWRnZQoJdlF1YWRFZGdlX1N0YWdlMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAABpAwAAaW4gZmxvYXQ0IHZRdWFkRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgloYWxmIGVkZ2VBbHBoYTsKCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TdGFnZTAueiA+IDAuMCAmJiB2UXVhZEVkZ2VfU3RhZ2UwLncgPiAwLjApIAoJewoJCWVkZ2VBbHBoYSA9IGhhbGYobWluKG1pbih2UXVhZEVkZ2VfU3RhZ2UwLnosIHZRdWFkRWRnZV9TdGFnZTAudykgKyAwLjUsIDEuMCkpOwoJfQoJZWxzZSAKCXsKCQloYWxmMiBnRiA9IGhhbGYyKGhhbGYoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSksICAgICAgICAgICAgICAgICBoYWxmKDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","K4JBYAQCAAGAAAAAAIWP577774BSZ7X7777QCAAAAABAAAAAABBAMACAAUAAAABAAAAAAABBEMAAA":"BAAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAqwYAAHVuaWZvcm0gaGFsZiB1U3JjVEZfU3RhZ2UwWzddOwp1bmlmb3JtIGhhbGYzeDMgdUNvbG9yWGZvcm1fU3RhZ2UwOwp1bmlmb3JtIGhhbGYgdURzdFRGX1N0YWdlMFs3XTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmIHNyY190Zl9TdGFnZTAoaGFsZiB4KSAKewoJaGFsZiBHID0gdVNyY1RGX1N0YWdlMFswXTsKCWhhbGYgQSA9IHVTcmNURl9TdGFnZTBbMV07CgloYWxmIEIgPSB1U3JjVEZfU3RhZ2UwWzJdOwoJaGFsZiBDID0gdVNyY1RGX1N0YWdlMFszXTsKCWhhbGYgRCA9IHVTcmNURl9TdGFnZTBbNF07CgloYWxmIEUgPSB1U3JjVEZfU3RhZ2UwWzVdOwoJaGFsZiBGID0gdVNyY1RGX1N0YWdlMFs2XTsKCWhhbGYgcyA9IHNpZ24oeCk7Cgl4ID0gYWJzKHgpOwoJeCA9ICh4IDwgRCkgPyAoQyAqIHgpICsgRiA6IHBvdyhBICogeCArIEIsIEcpICsgRTsKCXJldHVybiBzICogeDsKfQpoYWxmIGRzdF90Zl9TdGFnZTAoaGFsZiB4KSAKewoJaGFsZiBHID0gdURzdFRGX1N0YWdlMFswXTsKCWhhbGYgQSA9IHVEc3RURl9TdGFnZTBbMV07CgloYWxmIEIgPSB1RHN0VEZfU3RhZ2UwWzJdOwoJaGFsZiBDID0gdURzdFRGX1N0YWdlMFszXTsKCWhhbGYgRCA9IHVEc3RURl9TdGFnZTBbNF07CgloYWxmIEUgPSB1RHN0VEZfU3RhZ2UwWzVdOwoJaGFsZiBGID0gdURzdFRGX1N0YWdlMFs2XTsKCWhhbGYgcyA9IHNpZ24oeCk7Cgl4ID0gYWJzKHgpOwoJeCA9ICh4IDwgRCkgPyAoQyAqIHgpICsgRiA6IHBvdyhBICogeCArIEIsIEcpICsgRTsKCXJldHVybiBzICogeDsKfQpoYWxmNCBnYW11dF94Zm9ybV9TdGFnZTAoaGFsZjQgY29sb3IpIAp7Cgljb2xvci5yZ2IgPSAodUNvbG9yWGZvcm1fU3RhZ2UwICogY29sb3IucmdiKTsKCXJldHVybiBjb2xvcjsKfQpoYWxmNCBjb2xvcl94Zm9ybV9TdGFnZTAoZmxvYXQ0IGNvbG9yKSAKewoJY29sb3IuciA9IHNyY190Zl9TdGFnZTAoaGFsZihjb2xvci5yKSk7Cgljb2xvci5nID0gc3JjX3RmX1N0YWdlMChoYWxmKGNvbG9yLmcpKTsKCWNvbG9yLmIgPSBzcmNfdGZfU3RhZ2UwKGhhbGYoY29sb3IuYikpOwoJY29sb3IgPSBnYW11dF94Zm9ybV9TdGFnZTAoaGFsZjQoY29sb3IpKTsKCWNvbG9yLnIgPSBkc3RfdGZfU3RhZ2UwKGhhbGYoY29sb3IucikpOwoJY29sb3IuZyA9IGRzdF90Zl9TdGFnZTAoaGFsZihjb2xvci5nKSk7Cgljb2xvci5iID0gZHN0X3RmX1N0YWdlMChoYWxmKGNvbG9yLmIpKTsKCXJldHVybiBoYWxmNChjb2xvcik7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9ICgoY29sb3JfeGZvcm1fU3RhZ2UwKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","GFQQAAAAMAAGGADDACRQAAAAMAACHQDCABRQAI7CAMAAAABAA2JAOAAACAAAAAAAIACQAAAAEAAAAAAAEERQAAAAAA":"BAAAAExTS1PfCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQgYWFfYmxvYXRfbXVsdGlwbGllciA9IDE7CglmbG9hdDIgY29ybmVyID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy54eTsKCWZsb2F0MiByYWRpdXNfb3V0c2V0ID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy56dzsKCWZsb2F0MiBhYV9ibG9hdF9kaXJlY3Rpb24gPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UueHk7CglmbG9hdCBpc19saW5lYXJfY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UudzsKCWZsb2F0MiBwaXhlbGxlbmd0aCA9IGludmVyc2VzcXJ0KGZsb2F0Mihkb3Qoc2tldy54eiwgc2tldy54eiksIGRvdChza2V3Lnl3LCBza2V3Lnl3KSkpOwoJZmxvYXQ0IG5vcm1hbGl6ZWRfYXhpc19kaXJzID0gc2tldyAqIHBpeGVsbGVuZ3RoLnh5eHk7CglmbG9hdDIgYXhpc3dpZHRocyA9IChhYnMobm9ybWFsaXplZF9heGlzX2RpcnMueHkpICsgYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnp3KSk7CglmbG9hdDIgYWFfYmxvYXRyYWRpdXMgPSBheGlzd2lkdGhzICogcGl4ZWxsZW5ndGggKiAuNTsKCWZsb2F0NCByYWRpaV9hbmRfbmVpZ2hib3JzID0gcmFkaWlfc2VsZWN0b3IqIGZsb2F0NHg0KHJhZGlpX3gsIHJhZGlpX3ksIHJhZGlpX3gueXh3eiwgcmFkaWlfeS53enl4KTsKCWZsb2F0MiByYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMueHk7CglmbG9hdDIgbmVpZ2hib3JfcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnp3OwoJZmxvYXQgY292ZXJhZ2VfbXVsdGlwbGllciA9IDE7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2VfbXVsdGlwbGllciA9IDEgLyAobWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpKTsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCX0KCWZsb2F0IGNvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLno7CglpZiAoYW55KGxlc3NUaGFuKHJhZGlpLCBhYV9ibG9hdHJhZGl1cyAqIDEuNSkpKSAKCXsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSBzaWduKGNvcm5lcik7CgkJaWYgKGNvdmVyYWdlID4gLjUpIAoJCXsKCQkJYWFfYmxvYXRfZGlyZWN0aW9uID0gLWFhX2Jsb2F0X2RpcmVjdGlvbjsKCQl9CgkJaXNfbGluZWFyX2NvdmVyYWdlID0gMTsKCX0KCWVsc2UgCgl7CgkJcmFkaWkgPSBjbGFtcChyYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJbmVpZ2hib3JfcmFkaWkgPSBjbGFtcChuZWlnaGJvcl9yYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJZmxvYXQyIHNwYWNpbmcgPSAyIC0gcmFkaWkgLSBuZWlnaGJvcl9yYWRpaTsKCQlmbG9hdDIgZXh0cmFfcGFkID0gbWF4KHBpeGVsbGVuZ3RoICogLjA2MjUgLSBzcGFjaW5nLCBmbG9hdDIoMCkpOwoJCXJhZGlpIC09IGV4dHJhX3BhZCAqIC41OwoJfQoJZmxvYXQyIGFhX291dHNldCA9IGFhX2Jsb2F0X2RpcmVjdGlvbiAqIGFhX2Jsb2F0cmFkaXVzICogYWFfYmxvYXRfbXVsdGlwbGllcjsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglpZiAoY292ZXJhZ2UgPiAuNSkgCgl7CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi54ICE9IDAgJiYgdmVydGV4cG9zLnggKiBjb3JuZXIueCA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueCk7CgkJCXZlcnRleHBvcy54ID0gMDsKCQkJdmVydGV4cG9zLnkgKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLnkpICogcGl4ZWxsZW5ndGgueS9waXhlbGxlbmd0aC54OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueCkgLyAoYWJzKGNvcm5lci54KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueSAhPSAwICYmIHZlcnRleHBvcy55ICogY29ybmVyLnkgPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLnkpOwoJCQl2ZXJ0ZXhwb3MueSA9IDA7CgkJCXZlcnRleHBvcy54ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci54KSAqIHBpeGVsbGVuZ3RoLngvcGl4ZWxsZW5ndGgueTsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLnkpIC8gKGFicyhjb3JuZXIueSkgKyBiYWNrc2V0KSArIC41OwoJCX0KCX0KCWZsb2F0MngyIHNrZXdtYXRyaXggPSBmbG9hdDJ4Mihza2V3Lnh5LCBza2V3Lnp3KTsKCWZsb2F0MiBkZXZjb29yZCA9IHZlcnRleHBvcyAqIHNrZXdtYXRyaXggKyB0cmFuc2xhdGU7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TdGFnZTAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAABAQAAAAAAAAEBAFMEAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1N0YWdlMC54LCB5PXZhcmNjb29yZF9TdGFnZTAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","K4JAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAABAAAAAABBAMADCEB4QAAAAAEAAAAAAKQAAAAAAAIAAAAAQGIBAAAAA":"BAAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEApwMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTFfYzAuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTFfYzAueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdGV4Q29vcmQpICogaGFsZjQoMSkpKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBDaXJjdWxhclJSZWN0X1N0YWdlMV9jMChvdXRwdXRDb3ZlcmFnZV9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","DIQAAAAAMAAAAABAYAROFA2CAIAAAABAAAAAAAAAAAACABUSA4AAAEAAAAAAAQAFAAAAAIAAAAAAAIJDAAAA":"BAAAAExTS1NQAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBmbG9hdCB2VGV4SW5kZXhfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNTaXplSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSBmbG9hdCh0ZXhJZHgpOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChpblBvc2l0aW9uLnggLCBpblBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAMUDAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1N0YWdlMDsKaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB2VGV4dHVyZUNvb3Jkc19TdGFnZTApLnJycnI7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","K5JAAAAAAAMAAAAAARMPZ72HPQCFR7H7777QGAAAAACAAAAAACCAYAEABIAAAACAAAAAAACCIYAAA":"BAAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAAcAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfU3RhZ2UwKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQYAAAAAAQAAQAAAAGJCABAAACAACAAAAABAGOAQAAQAAAABQCVAEQAGAAAAAAAAAAAVAAAAAAAAQAAAABAMQCAAAAA":"BAAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAcFAAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs3XTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMF9jMDsKdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMF9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwX2MwX2MwLngsIHVjbGFtcF9TdGFnZTFfYzBfYzBfYzBfYzAueik7CgljbGFtcGVkQ29vcmQueSA9IHN1YnNldENvb3JkLnk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzBfYzAoX2lucHV0LCAoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwX2MwKSAqIF9jb29yZHMueHkxKS54eSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgXzJfY29sb3IgPSBoYWxmNCgwLjApOwoJZmxvYXQyIF8zX2Nvb3JkID0gKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCAtIGZsb2F0MigoMTIuMCAqIHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNl0ueCkpOwoJcmV0dXJuIF8yX2NvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fU3RhZ2UxX2MwX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","K5IACAAAAAMAAAAAARMAAVCEPQCFR7H7777QGAAAAAAAAAAAWRDQB4AA6AAPAAHQDQAAAAAAEIFQAAAADQGQAAAAQCHAIAAAADMBAAAAAAABKAAAABAACAAAACCIYAAA":"BAAAAExTS1MiAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0IHZjb3ZlcmFnZV9TdGFnZTA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBwb3NpdGlvbiA9IHBvc2l0aW9uLnh5OwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJdmNvdmVyYWdlX1N0YWdlMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwX2MwKSkgKiBsb2NhbENvb3JkLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAABIBwAAdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXN0YXJ0X1N0YWdlMV9jMF9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1ZW5kX1N0YWdlMV9jMF9jMF9jMTsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwppbiBmbG9hdCB2Y292ZXJhZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBMaW5lYXJHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIGhhbGY0KGhhbGYodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzBfYzEsIGhhbGYoX2Nvb3Jkcy54KSk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCW91dENvbG9yID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsR3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMC4wKSkpOwoJfQoJQGlmICh0cnVlKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBvdXRDb2xvcjsKfQpoYWxmNCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3JfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwX2MwKGZhbHNlID8gaGFsZjQoMCkgOiBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCkpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3Nvcl9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSAoaGFsZjQoMS4wKSAtIG91dHB1dF9TdGFnZTEpICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQCAACAAAABGAIEBSAAIAAAAAAAAAACUAAAAEAAAAAAAEERQAAAAA":"BAAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAADsAwAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZC54ID0gY2xhbXAoc3Vic2V0Q29vcmQueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC54LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","BYIBQAAAAQAAAAABCYIR7777777QAAAAAAAAAAAAUABAAAAACAAAAAEASAIQAAAA":"BAAAAExTS1NnAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBjb2xvciA9IGluQ29sb3I7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAOgEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgABAAAAAAAAAA==","GFQQAAAAMAAGGADDACRQAAAAMAACHQDCABRQAI7CAMAAAABAC5JAAAAAAAABKAAAACAAAAAAACCIYAAAAAAA":"BAAAAExTS1PfCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQgYWFfYmxvYXRfbXVsdGlwbGllciA9IDE7CglmbG9hdDIgY29ybmVyID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy54eTsKCWZsb2F0MiByYWRpdXNfb3V0c2V0ID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy56dzsKCWZsb2F0MiBhYV9ibG9hdF9kaXJlY3Rpb24gPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UueHk7CglmbG9hdCBpc19saW5lYXJfY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UudzsKCWZsb2F0MiBwaXhlbGxlbmd0aCA9IGludmVyc2VzcXJ0KGZsb2F0Mihkb3Qoc2tldy54eiwgc2tldy54eiksIGRvdChza2V3Lnl3LCBza2V3Lnl3KSkpOwoJZmxvYXQ0IG5vcm1hbGl6ZWRfYXhpc19kaXJzID0gc2tldyAqIHBpeGVsbGVuZ3RoLnh5eHk7CglmbG9hdDIgYXhpc3dpZHRocyA9IChhYnMobm9ybWFsaXplZF9heGlzX2RpcnMueHkpICsgYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnp3KSk7CglmbG9hdDIgYWFfYmxvYXRyYWRpdXMgPSBheGlzd2lkdGhzICogcGl4ZWxsZW5ndGggKiAuNTsKCWZsb2F0NCByYWRpaV9hbmRfbmVpZ2hib3JzID0gcmFkaWlfc2VsZWN0b3IqIGZsb2F0NHg0KHJhZGlpX3gsIHJhZGlpX3ksIHJhZGlpX3gueXh3eiwgcmFkaWlfeS53enl4KTsKCWZsb2F0MiByYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMueHk7CglmbG9hdDIgbmVpZ2hib3JfcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnp3OwoJZmxvYXQgY292ZXJhZ2VfbXVsdGlwbGllciA9IDE7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2VfbXVsdGlwbGllciA9IDEgLyAobWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpKTsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCX0KCWZsb2F0IGNvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLno7CglpZiAoYW55KGxlc3NUaGFuKHJhZGlpLCBhYV9ibG9hdHJhZGl1cyAqIDEuNSkpKSAKCXsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSBzaWduKGNvcm5lcik7CgkJaWYgKGNvdmVyYWdlID4gLjUpIAoJCXsKCQkJYWFfYmxvYXRfZGlyZWN0aW9uID0gLWFhX2Jsb2F0X2RpcmVjdGlvbjsKCQl9CgkJaXNfbGluZWFyX2NvdmVyYWdlID0gMTsKCX0KCWVsc2UgCgl7CgkJcmFkaWkgPSBjbGFtcChyYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJbmVpZ2hib3JfcmFkaWkgPSBjbGFtcChuZWlnaGJvcl9yYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJZmxvYXQyIHNwYWNpbmcgPSAyIC0gcmFkaWkgLSBuZWlnaGJvcl9yYWRpaTsKCQlmbG9hdDIgZXh0cmFfcGFkID0gbWF4KHBpeGVsbGVuZ3RoICogLjA2MjUgLSBzcGFjaW5nLCBmbG9hdDIoMCkpOwoJCXJhZGlpIC09IGV4dHJhX3BhZCAqIC41OwoJfQoJZmxvYXQyIGFhX291dHNldCA9IGFhX2Jsb2F0X2RpcmVjdGlvbiAqIGFhX2Jsb2F0cmFkaXVzICogYWFfYmxvYXRfbXVsdGlwbGllcjsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglpZiAoY292ZXJhZ2UgPiAuNSkgCgl7CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi54ICE9IDAgJiYgdmVydGV4cG9zLnggKiBjb3JuZXIueCA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueCk7CgkJCXZlcnRleHBvcy54ID0gMDsKCQkJdmVydGV4cG9zLnkgKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLnkpICogcGl4ZWxsZW5ndGgueS9waXhlbGxlbmd0aC54OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueCkgLyAoYWJzKGNvcm5lci54KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueSAhPSAwICYmIHZlcnRleHBvcy55ICogY29ybmVyLnkgPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLnkpOwoJCQl2ZXJ0ZXhwb3MueSA9IDA7CgkJCXZlcnRleHBvcy54ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci54KSAqIHBpeGVsbGVuZ3RoLngvcGl4ZWxsZW5ndGgueTsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLnkpIC8gKGFicyhjb3JuZXIueSkgKyBiYWNrc2V0KSArIC41OwoJCX0KCX0KCWZsb2F0MngyIHNrZXdtYXRyaXggPSBmbG9hdDJ4Mihza2V3Lnh5LCBza2V3Lnp3KTsKCWZsb2F0MiBkZXZjb29yZCA9IHZlcnRleHBvcyAqIHNrZXdtYXRyaXggKyB0cmFuc2xhdGU7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TdGFnZTAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAABAQAAAAAAAAEBAHsFAAB1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJaGFsZiBjb3ZlcmFnZTsKCUBzd2l0Y2ggKDEpIAoJewoJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQlicmVhazsKCQlkZWZhdWx0OiAgICAgICAgaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCUBpZiAoMSA9PSAyIHx8IDEgPT0gMykgCgl7CgkJY292ZXJhZ2UgPSAxLjAgLSBjb3ZlcmFnZTsKCX0KCXJldHVybiBfaW5wdXQgKiBjb3ZlcmFnZTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1N0YWdlMC54LCB5PXZhcmNjb29yZF9TdGFnZTAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","K4QAAAAAAAGAAAAAAIWP57ZDH37P7777777QCAAAAAAAAAAAIACQAAAAEAAAAAAAEERQAAA":"BAAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAEUBAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","KYNQAAAABCYIR6AYYAAAAAAAAAAAAADIR4AOAAPAAHQADYBZAAAAAACECYAAAABYDIAAAAAADUEQAAAAWAQQAAAAAAVAAAAAAAAQAAAABAMQC":"BAAAAExTS1OgAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TdGFnZTAgPSBpblF1YWRFZGdlOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAFwkAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzBfYzE7CmluIGZsb2F0NCB2UXVhZEVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gaGFsZjQoaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2LCAxLjAsIDAuMCwgMC4wKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBMaW5lYXJHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIG1peCh1c3RhcnRfU3RhZ2UxX2MwX2MwX2MxLCB1ZW5kX1N0YWdlMV9jMF9jMF9jMSwgaGFsZihfY29vcmRzLngpKTsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMF9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJb3V0Q29sb3IgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIG91dENvbG9yOwp9CmhhbGY0IE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3Nvcl9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzBfYzAoZmFsc2UgPyBoYWxmNCgwKSA6IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKSk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgloYWxmIGVkZ2VBbHBoYTsKCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TdGFnZTAueiA+IDAuMCAmJiB2UXVhZEVkZ2VfU3RhZ2UwLncgPiAwLjApIAoJewoJCWVkZ2VBbHBoYSA9IGhhbGYobWluKG1pbih2UXVhZEVkZ2VfU3RhZ2UwLnosIHZRdWFkRWRnZV9TdGFnZTAudykgKyAwLjUsIDEuMCkpOwoJfQoJZWxzZSAKCXsKCQloYWxmMiBnRiA9IGhhbGYyKGhhbGYoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSksICAgICAgICAgICAgICAgICBoYWxmKDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpblF1YWRFZGdlAAABAAAAAAAAAA==","AWAQGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAAAUABAAAAACAAAAAEBSAI":"BAAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAALcCAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","GFQQAAAAMAAGGADDACRQAAAAMAACHQDCABRQAI7CAMAAAABAA2JAOAAAKAAAAAGAQUKAAAAAABAAKAAAAAQAAAAAAAQSGAAA":"BAAAAExTS1PfCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQgYWFfYmxvYXRfbXVsdGlwbGllciA9IDE7CglmbG9hdDIgY29ybmVyID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy54eTsKCWZsb2F0MiByYWRpdXNfb3V0c2V0ID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy56dzsKCWZsb2F0MiBhYV9ibG9hdF9kaXJlY3Rpb24gPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UueHk7CglmbG9hdCBpc19saW5lYXJfY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UudzsKCWZsb2F0MiBwaXhlbGxlbmd0aCA9IGludmVyc2VzcXJ0KGZsb2F0Mihkb3Qoc2tldy54eiwgc2tldy54eiksIGRvdChza2V3Lnl3LCBza2V3Lnl3KSkpOwoJZmxvYXQ0IG5vcm1hbGl6ZWRfYXhpc19kaXJzID0gc2tldyAqIHBpeGVsbGVuZ3RoLnh5eHk7CglmbG9hdDIgYXhpc3dpZHRocyA9IChhYnMobm9ybWFsaXplZF9heGlzX2RpcnMueHkpICsgYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnp3KSk7CglmbG9hdDIgYWFfYmxvYXRyYWRpdXMgPSBheGlzd2lkdGhzICogcGl4ZWxsZW5ndGggKiAuNTsKCWZsb2F0NCByYWRpaV9hbmRfbmVpZ2hib3JzID0gcmFkaWlfc2VsZWN0b3IqIGZsb2F0NHg0KHJhZGlpX3gsIHJhZGlpX3ksIHJhZGlpX3gueXh3eiwgcmFkaWlfeS53enl4KTsKCWZsb2F0MiByYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMueHk7CglmbG9hdDIgbmVpZ2hib3JfcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnp3OwoJZmxvYXQgY292ZXJhZ2VfbXVsdGlwbGllciA9IDE7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2VfbXVsdGlwbGllciA9IDEgLyAobWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpKTsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCX0KCWZsb2F0IGNvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLno7CglpZiAoYW55KGxlc3NUaGFuKHJhZGlpLCBhYV9ibG9hdHJhZGl1cyAqIDEuNSkpKSAKCXsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSBzaWduKGNvcm5lcik7CgkJaWYgKGNvdmVyYWdlID4gLjUpIAoJCXsKCQkJYWFfYmxvYXRfZGlyZWN0aW9uID0gLWFhX2Jsb2F0X2RpcmVjdGlvbjsKCQl9CgkJaXNfbGluZWFyX2NvdmVyYWdlID0gMTsKCX0KCWVsc2UgCgl7CgkJcmFkaWkgPSBjbGFtcChyYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJbmVpZ2hib3JfcmFkaWkgPSBjbGFtcChuZWlnaGJvcl9yYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJZmxvYXQyIHNwYWNpbmcgPSAyIC0gcmFkaWkgLSBuZWlnaGJvcl9yYWRpaTsKCQlmbG9hdDIgZXh0cmFfcGFkID0gbWF4KHBpeGVsbGVuZ3RoICogLjA2MjUgLSBzcGFjaW5nLCBmbG9hdDIoMCkpOwoJCXJhZGlpIC09IGV4dHJhX3BhZCAqIC41OwoJfQoJZmxvYXQyIGFhX291dHNldCA9IGFhX2Jsb2F0X2RpcmVjdGlvbiAqIGFhX2Jsb2F0cmFkaXVzICogYWFfYmxvYXRfbXVsdGlwbGllcjsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglpZiAoY292ZXJhZ2UgPiAuNSkgCgl7CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi54ICE9IDAgJiYgdmVydGV4cG9zLnggKiBjb3JuZXIueCA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueCk7CgkJCXZlcnRleHBvcy54ID0gMDsKCQkJdmVydGV4cG9zLnkgKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLnkpICogcGl4ZWxsZW5ndGgueS9waXhlbGxlbmd0aC54OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueCkgLyAoYWJzKGNvcm5lci54KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueSAhPSAwICYmIHZlcnRleHBvcy55ICogY29ybmVyLnkgPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLnkpOwoJCQl2ZXJ0ZXhwb3MueSA9IDA7CgkJCXZlcnRleHBvcy54ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci54KSAqIHBpeGVsbGVuZ3RoLngvcGl4ZWxsZW5ndGgueTsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLnkpIC8gKGFicyhjb3JuZXIueSkgKyBiYWNrc2V0KSArIC41OwoJCX0KCX0KCWZsb2F0MngyIHNrZXdtYXRyaXggPSBmbG9hdDJ4Mihza2V3Lnh5LCBza2V3Lnp3KTsKCWZsb2F0MiBkZXZjb29yZCA9IHZlcnRleHBvcyAqIHNrZXdtYXRyaXggKyB0cmFuc2xhdGU7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TdGFnZTAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAABAQAAAAAAAAEBACMHAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJaGFsZiBjb3ZlcmFnZTsKCUBzd2l0Y2ggKDEpIAoJewoJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwX2MwLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTFfYzBfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQlicmVhazsKCQlkZWZhdWx0OiAgICAgICAgaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwX2MwKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCUBpZiAoMSA9PSAyIHx8IDEgPT0gMykgCgl7CgkJY292ZXJhZ2UgPSAxLjAgLSBjb3ZlcmFnZTsKCX0KCXJldHVybiBfaW5wdXQgKiBjb3ZlcmFnZTsKfQpoYWxmNCBDaXJjdWxhclJSZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCkgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1N0YWdlMC54LCB5PXZhcmNjb29yZF9TdGFnZTAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","GFQQAAAAMAAGGADDACRQAAAAMAACHQDCABRQAI7CAMAAAABAAYJAMAAACAAAAAAAIACQAAAAEAAAAAAAEERQAAAAAA":"BAAAAExTS1PfCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQgYWFfYmxvYXRfbXVsdGlwbGllciA9IDE7CglmbG9hdDIgY29ybmVyID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy54eTsKCWZsb2F0MiByYWRpdXNfb3V0c2V0ID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy56dzsKCWZsb2F0MiBhYV9ibG9hdF9kaXJlY3Rpb24gPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UueHk7CglmbG9hdCBpc19saW5lYXJfY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UudzsKCWZsb2F0MiBwaXhlbGxlbmd0aCA9IGludmVyc2VzcXJ0KGZsb2F0Mihkb3Qoc2tldy54eiwgc2tldy54eiksIGRvdChza2V3Lnl3LCBza2V3Lnl3KSkpOwoJZmxvYXQ0IG5vcm1hbGl6ZWRfYXhpc19kaXJzID0gc2tldyAqIHBpeGVsbGVuZ3RoLnh5eHk7CglmbG9hdDIgYXhpc3dpZHRocyA9IChhYnMobm9ybWFsaXplZF9heGlzX2RpcnMueHkpICsgYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnp3KSk7CglmbG9hdDIgYWFfYmxvYXRyYWRpdXMgPSBheGlzd2lkdGhzICogcGl4ZWxsZW5ndGggKiAuNTsKCWZsb2F0NCByYWRpaV9hbmRfbmVpZ2hib3JzID0gcmFkaWlfc2VsZWN0b3IqIGZsb2F0NHg0KHJhZGlpX3gsIHJhZGlpX3ksIHJhZGlpX3gueXh3eiwgcmFkaWlfeS53enl4KTsKCWZsb2F0MiByYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMueHk7CglmbG9hdDIgbmVpZ2hib3JfcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnp3OwoJZmxvYXQgY292ZXJhZ2VfbXVsdGlwbGllciA9IDE7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2VfbXVsdGlwbGllciA9IDEgLyAobWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpKTsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCX0KCWZsb2F0IGNvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLno7CglpZiAoYW55KGxlc3NUaGFuKHJhZGlpLCBhYV9ibG9hdHJhZGl1cyAqIDEuNSkpKSAKCXsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSBzaWduKGNvcm5lcik7CgkJaWYgKGNvdmVyYWdlID4gLjUpIAoJCXsKCQkJYWFfYmxvYXRfZGlyZWN0aW9uID0gLWFhX2Jsb2F0X2RpcmVjdGlvbjsKCQl9CgkJaXNfbGluZWFyX2NvdmVyYWdlID0gMTsKCX0KCWVsc2UgCgl7CgkJcmFkaWkgPSBjbGFtcChyYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJbmVpZ2hib3JfcmFkaWkgPSBjbGFtcChuZWlnaGJvcl9yYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJZmxvYXQyIHNwYWNpbmcgPSAyIC0gcmFkaWkgLSBuZWlnaGJvcl9yYWRpaTsKCQlmbG9hdDIgZXh0cmFfcGFkID0gbWF4KHBpeGVsbGVuZ3RoICogLjA2MjUgLSBzcGFjaW5nLCBmbG9hdDIoMCkpOwoJCXJhZGlpIC09IGV4dHJhX3BhZCAqIC41OwoJfQoJZmxvYXQyIGFhX291dHNldCA9IGFhX2Jsb2F0X2RpcmVjdGlvbiAqIGFhX2Jsb2F0cmFkaXVzICogYWFfYmxvYXRfbXVsdGlwbGllcjsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglpZiAoY292ZXJhZ2UgPiAuNSkgCgl7CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi54ICE9IDAgJiYgdmVydGV4cG9zLnggKiBjb3JuZXIueCA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueCk7CgkJCXZlcnRleHBvcy54ID0gMDsKCQkJdmVydGV4cG9zLnkgKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLnkpICogcGl4ZWxsZW5ndGgueS9waXhlbGxlbmd0aC54OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueCkgLyAoYWJzKGNvcm5lci54KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueSAhPSAwICYmIHZlcnRleHBvcy55ICogY29ybmVyLnkgPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLnkpOwoJCQl2ZXJ0ZXhwb3MueSA9IDA7CgkJCXZlcnRleHBvcy54ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci54KSAqIHBpeGVsbGVuZ3RoLngvcGl4ZWxsZW5ndGgueTsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLnkpIC8gKGFicyhjb3JuZXIueSkgKyBiYWNrc2V0KSArIC41OwoJCX0KCX0KCWZsb2F0MngyIHNrZXdtYXRyaXggPSBmbG9hdDJ4Mihza2V3Lnh5LCBza2V3Lnp3KTsKCWZsb2F0MiBkZXZjb29yZCA9IHZlcnRleHBvcyAqIHNrZXdtYXRyaXggKyB0cmFuc2xhdGU7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TdGFnZTAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAABAQAAAAAAAAEBALUEAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdCBkeDAgPSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5MIC0gc2tfRnJhZ0Nvb3JkLng7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxX2MwLlJCOwoJZmxvYXQyIGR4eSA9IG1heChmbG9hdDIobWF4KGR4MCwgZHh5MS54KSwgZHh5MS55KSwgMC4wKTsKCWhhbGYgdG9wQWxwaGEgPSBoYWxmKHNhdHVyYXRlKHNrX0ZyYWdDb29yZC55IC0gdWlubmVyUmVjdF9TdGFnZTFfYzAuVCkpOwoJaGFsZiBhbHBoYSA9IHRvcEFscGhhICogaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBHckZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJaGFsZiBjb3ZlcmFnZTsKCWlmICgwID09IHhfcGx1c18xKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoeSk7Cgl9CgllbHNlIAoJewoJCWZsb2F0IGZuID0geF9wbHVzXzEgKiAoeF9wbHVzXzEgLSAyKTsKCQlmbiA9IGZtYSh5LHksIGZuKTsKCQlmbG9hdCBmbndpZHRoID0gZndpZHRoKGZuKTsKCQljb3ZlcmFnZSA9IC41IC0gaGFsZihmbi9mbndpZHRoKTsKCQljb3ZlcmFnZSA9IGNsYW1wKGNvdmVyYWdlLCAwLCAxKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQCAICAAAAA5BIEBAAAAAAAAJQDBAMAACAIAAAAAAKAAQAAAABAAAAACAJACIAAAAA":"BAAAAExTS1OjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMF9jMCkgKiAodW1hdHJpeF9TdGFnZTFfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAXQQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQgPSBjbGFtcChzdWJzZXRDb29yZCwgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMC54eSwgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","GFYQAAAAMAAGGADDACRQAAAAMAACHQDCABRQAI7CAMAAAAAAKQAAAAAAAIAAAAAQGIBAAAA":"BAAAAExTS1PfCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQgYWFfYmxvYXRfbXVsdGlwbGllciA9IDA7CglmbG9hdDIgY29ybmVyID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy54eTsKCWZsb2F0MiByYWRpdXNfb3V0c2V0ID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy56dzsKCWZsb2F0MiBhYV9ibG9hdF9kaXJlY3Rpb24gPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UueHk7CglmbG9hdCBpc19saW5lYXJfY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UudzsKCWZsb2F0MiBwaXhlbGxlbmd0aCA9IGludmVyc2VzcXJ0KGZsb2F0Mihkb3Qoc2tldy54eiwgc2tldy54eiksIGRvdChza2V3Lnl3LCBza2V3Lnl3KSkpOwoJZmxvYXQ0IG5vcm1hbGl6ZWRfYXhpc19kaXJzID0gc2tldyAqIHBpeGVsbGVuZ3RoLnh5eHk7CglmbG9hdDIgYXhpc3dpZHRocyA9IChhYnMobm9ybWFsaXplZF9heGlzX2RpcnMueHkpICsgYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnp3KSk7CglmbG9hdDIgYWFfYmxvYXRyYWRpdXMgPSBheGlzd2lkdGhzICogcGl4ZWxsZW5ndGggKiAuNTsKCWZsb2F0NCByYWRpaV9hbmRfbmVpZ2hib3JzID0gcmFkaWlfc2VsZWN0b3IqIGZsb2F0NHg0KHJhZGlpX3gsIHJhZGlpX3ksIHJhZGlpX3gueXh3eiwgcmFkaWlfeS53enl4KTsKCWZsb2F0MiByYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMueHk7CglmbG9hdDIgbmVpZ2hib3JfcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnp3OwoJZmxvYXQgY292ZXJhZ2VfbXVsdGlwbGllciA9IDE7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2VfbXVsdGlwbGllciA9IDEgLyAobWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpKTsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCX0KCWZsb2F0IGNvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLno7CglpZiAoYW55KGxlc3NUaGFuKHJhZGlpLCBhYV9ibG9hdHJhZGl1cyAqIDEuNSkpKSAKCXsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSBzaWduKGNvcm5lcik7CgkJaWYgKGNvdmVyYWdlID4gLjUpIAoJCXsKCQkJYWFfYmxvYXRfZGlyZWN0aW9uID0gLWFhX2Jsb2F0X2RpcmVjdGlvbjsKCQl9CgkJaXNfbGluZWFyX2NvdmVyYWdlID0gMTsKCX0KCWVsc2UgCgl7CgkJcmFkaWkgPSBjbGFtcChyYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJbmVpZ2hib3JfcmFkaWkgPSBjbGFtcChuZWlnaGJvcl9yYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJZmxvYXQyIHNwYWNpbmcgPSAyIC0gcmFkaWkgLSBuZWlnaGJvcl9yYWRpaTsKCQlmbG9hdDIgZXh0cmFfcGFkID0gbWF4KHBpeGVsbGVuZ3RoICogLjA2MjUgLSBzcGFjaW5nLCBmbG9hdDIoMCkpOwoJCXJhZGlpIC09IGV4dHJhX3BhZCAqIC41OwoJfQoJZmxvYXQyIGFhX291dHNldCA9IGFhX2Jsb2F0X2RpcmVjdGlvbiAqIGFhX2Jsb2F0cmFkaXVzICogYWFfYmxvYXRfbXVsdGlwbGllcjsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglpZiAoY292ZXJhZ2UgPiAuNSkgCgl7CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi54ICE9IDAgJiYgdmVydGV4cG9zLnggKiBjb3JuZXIueCA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueCk7CgkJCXZlcnRleHBvcy54ID0gMDsKCQkJdmVydGV4cG9zLnkgKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLnkpICogcGl4ZWxsZW5ndGgueS9waXhlbGxlbmd0aC54OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueCkgLyAoYWJzKGNvcm5lci54KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueSAhPSAwICYmIHZlcnRleHBvcy55ICogY29ybmVyLnkgPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLnkpOwoJCQl2ZXJ0ZXhwb3MueSA9IDA7CgkJCXZlcnRleHBvcy54ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci54KSAqIHBpeGVsbGVuZ3RoLngvcGl4ZWxsZW5ndGgueTsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLnkpIC8gKGFicyhjb3JuZXIueSkgKyBiYWNrc2V0KSArIC41OwoJCX0KCX0KCWZsb2F0MngyIHNrZXdtYXRyaXggPSBmbG9hdDJ4Mihza2V3Lnh5LCBza2V3Lnp3KTsKCWZsb2F0MiBkZXZjb29yZCA9IHZlcnRleHBvcyAqIHNrZXdtYXRyaXggKyB0cmFuc2xhdGU7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TdGFnZTAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAK0CAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1N0YWdlMC54LCB5PXZhcmNjb29yZF9TdGFnZTAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9Cgljb3ZlcmFnZSA9IChjb3ZlcmFnZSA+PSAuNSkgPyAxIDogMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGNvdmVyYWdlKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","K4JAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAABAAAAAABBAMAHSEECQAAAAABIACAAAAAEAAAAAIDEAQAAAAA":"BAAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEAZQQAAHVuaWZvcm0gZmxvYXQ0IHVjaXJjbGVfU3RhZ2UxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmNsZUVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIHByZXZDZW50ZXI7CglmbG9hdCBwcmV2UmFkaXVzID0gLTEuMDAwMDAwOwoJaGFsZiBkOwoJQGlmICgxID09IDIgfHwgMSA9PSAzKSAKCXsKCQlkID0gaGFsZigobGVuZ3RoKCh1Y2lyY2xlX1N0YWdlMV9jMC54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1N0YWdlMV9jMC53KSAtIDEuMCkgKiB1Y2lyY2xlX1N0YWdlMV9jMC56KTsKCX0KCWVsc2UgCgl7CgkJZCA9IGhhbGYoKDEuMCAtIGxlbmd0aCgodWNpcmNsZV9TdGFnZTFfYzAueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TdGFnZTFfYzAudykpICogdWNpcmNsZV9TdGFnZTFfYzAueik7Cgl9CgloYWxmNCBpbnB1dENvbG9yID0gX2lucHV0OwoJQGlmICgxID09IDEgfHwgMSA9PSAzKSAKCXsKCQlyZXR1cm4gaW5wdXRDb2xvciAqIGNsYW1wKGQsIDAuMCwgMS4wKTsKCX0KCWVsc2UgCgl7CgkJcmV0dXJuIGQgPiAwLjUgPyBpbnB1dENvbG9yIDogaGFsZjQoMC4wKTsKCX0KfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IENpcmNsZUVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","B2ABSAAAAQAAAAABC3777777AAOAAAAAAAAAAAAAUABAAAAACAAAAAEASAIQAAAA":"BAAAAExTS1OpAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGYgaW5Db3ZlcmFnZTsKb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgY29sb3IgPSB1Q29sb3JfU3RhZ2UwOwoJY29sb3IgPSBjb2xvciAqIGluQ292ZXJhZ2U7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAA6AQAAaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","K6QACAAAAAGAAAAAAIWAAKRCH37P6BZQ737QCAAAAAAAAAAAIACQAAAAEAAAAAAAEERQAAA":"BAAAAExTS1O9AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0IHZjb3ZlcmFnZV9TdGFnZTA7CmZsYXQgb3V0IGZsb2F0NCB2Z2VvbVN1YnNldF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2Y292ZXJhZ2VfU3RhZ2UwID0gY292ZXJhZ2U7Cgl2Z2VvbVN1YnNldF9TdGFnZTAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAABAQAAAAAAAAEBAL0CAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TdGFnZTA7CmZsYXQgaW4gZmxvYXQ0IHZnZW9tU3Vic2V0X1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCWZsb2F0IGNvdmVyYWdlID0gdmNvdmVyYWdlX1N0YWdlMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TdGFnZTA7CgloYWxmNCBkaXN0czQgPSBjbGFtcChoYWxmNCgxLCAxLCAtMSwgLTEpICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSBnZW9TdWJzZXQpLCAwLCAxKTsKCWhhbGYyIGRpc3RzMiA9IGRpc3RzNC54eSArIGRpc3RzNC56dyAtIDE7CgloYWxmIHN1YnNldENvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCWNvdmVyYWdlID0gbWluKGNvdmVyYWdlLCBzdWJzZXRDb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGdlb21TdWJzZXQAAAEAAAAAAAAA","AWQQGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAIAXCIAEAAAAAAKQAAAAAAAIAAAAAQGIBAA":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQCrBQAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgloYWxmIGNvdmVyYWdlOwoJQHN3aXRjaCAoMSkgCgl7CgkJY2FzZSAwOiAgICBjYXNlIDI6ICAgICAgICBjb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1N0YWdlMV9jMC54eSwgc2tfRnJhZ0Nvb3JkLnh5KSkpID8gMSA6IDApOwoJCWJyZWFrOwoJCWRlZmF1bHQ6ICAgICAgICBoYWxmNCBkaXN0czQgPSBjbGFtcChoYWxmNCgxLjAsIDEuMCwgLTEuMCwgLTEuMCkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzApLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJQGlmICgxID09IDIgfHwgMSA9PSAzKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIF9pbnB1dCAqIGNvdmVyYWdlOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","BYAAQAAAAQAAAAABC3777777777QAAAAAAAAAAAAUABAAAAACAAAAAEASAIQAAAA":"BAAAAExTS1PkAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAPwEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAKAAAAaW5Qb3NpdGlvbgAAAQAAAAAAAAA=","AWQQGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAAAUABAAAAACAAAAAEBSAI":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAC3AgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","K4QACAAAAAGAAAAAAIWP57ZDH37P7777777QCAAAAAAAAAAAIACQAAAAEAAAAAAAEERQAAA":"BAAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAQAEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","AWQAGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAIAGGIDYAAAQAAAAAAAAAVAAAABAAAAAAABBEMAAAA":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQAIBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTFfYzAuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxX2MwLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwLnggLSBsZW5ndGgoZHh5KSkpOwoJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBDaXJjdWxhclJSZWN0X1N0YWdlMV9jMChvdXRwdXRDb3ZlcmFnZV9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQYAAAAAAAAAQAAAAGJCABAAAAAACAAAAABAGOAAAAQAAAABQCVAEQAEAAAAAAAAAAAVAAAAAAAAQAAAABAMQCAAAAA":"BAAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAADIEgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs3XTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMF9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIF9jb29yZHMpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMF9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzBfYzBfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKfQpoYWxmNCBHYXVzc2lhbkNvbnZvbHV0aW9uX1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfMl9jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzNfY29vcmQgPSAodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwIC0gZmxvYXQyKCgxMi4wICogdV8wX0luY3JlbWVudF9TdGFnZTFfYzBfYzApKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFswXS54KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFswXS55KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFswXS56KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFswXS53KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsxXS54KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsxXS55KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsxXS56KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsxXS53KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsyXS54KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsyXS55KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsyXS56KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsyXS53KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFszXS54KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFszXS55KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFszXS56KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFszXS53KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs0XS54KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs0XS55KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs0XS56KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs0XS53KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs1XS54KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs1XS55KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs1XS56KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs1XS53KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs2XS54KSk7CglyZXR1cm4gXzJfY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gR2F1c3NpYW5Db252b2x1dGlvbl9TdGFnZTFfYzBfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","IYBQAAAAAELBCHYCDYAAAAAAAEAAAACAIQAABIACAAAAAEAAAAAIBEARAA":"BAAAAExTS1OFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFJSZWN0U2hhZG93Cgl2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABPAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBoYWxmMyB2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCWhhbGYgZCA9IGxlbmd0aChzaGFkb3dQYXJhbXMueHkpOwoJZmxvYXQyIHV2ID0gZmxvYXQyKHNoYWRvd1BhcmFtcy56ICogKDEuMCAtIGQpLCAwLjUpOwoJaGFsZiBmYWN0b3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB1dikuMDAwci5hOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA4AAABpblNoYWRvd1BhcmFtcwAAAQAAAAAAAAA=","B2IASAAAAQAAAAABCYIR7777AAOAAAAAAAAAAAAAUABAAAAACAAAAAEASAIQAAAA":"BAAAAExTS1NwAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCWNvbG9yID0gY29sb3IgKiBpbkNvdmVyYWdlOwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAA6AQAAaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQCAICAAAAA5BIEBAAAAAAAAJQDBAMAACAIAAAAAAKAAQAAAABAAAAACAJAEIAAAAA":"BAAAAExTS1OjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMF9jMCkgKiAodW1hdHJpeF9TdGFnZTFfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAXQQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQgPSBjbGFtcChzdWJzZXRDb29yZCwgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMC54eSwgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","DISAAAAAMAAAAABAYDRP7H2CAIAAAABAAAAAAABAMQAAAVAAAAAAAAQAAAABAMQC":"BAAAAExTS1MHAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBmbG9hdCB2VGV4SW5kZXhfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNTaXplSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAADACAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1N0YWdlMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKTsKCX0KCW91dHB1dENvbG9yX1N0YWdlMCA9IG91dHB1dENvbG9yX1N0YWdlMCAqIHRleENvbG9yOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","AWQAGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAAAUABAAAAACAAAAAEBSAI":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAAmAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQCAICAAAABGAIEBSAAIBAAAAAAAAACUAAAAEAAAAAAAEERQAAAAA":"BAAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAADJAwAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfU3RhZ2UxX2MwX2MwLnh5LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","K5JAAAAAAAMAAAAAARMPZ72HPQCFR7H7777QGAAAAACAAAAAACCAYAGEIDZAAAAAAIAAAAAAVAAAAAAAAQAAAABAMQCAAAAA":"BAAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAQEAAAAAAAABAQDoAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTFfYzAuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxX2MwLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfU3RhZ2UwKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","K5IAAAAAAAMAAAAAARMPZ72HPQCFR7H7777QGAAAAAAAAAAAWRDQB4AA6AAPAAHQDQAAAAAAEIFQAAAADQGQAAAAQCHAIAAAADMBAAAAAAABKAAAACAAAAAAACCIYAAA":"BAAAAExTS1O1AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzBfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAAADzBgAAdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXN0YXJ0X1N0YWdlMV9jMF9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1ZW5kX1N0YWdlMV9jMF9jMF9jMTsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBMaW5lYXJHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIGhhbGY0KGhhbGYodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzBfYzEsIGhhbGYoX2Nvb3Jkcy54KSk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCW91dENvbG9yID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsR3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMC4wKSkpOwoJfQoJQGlmICh0cnVlKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBvdXRDb2xvcjsKfQpoYWxmNCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3JfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwX2MwKGZhbHNlID8gaGFsZjQoMCkgOiBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCkpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AWQAGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAIAXGIAEAAAAAAKQAAAAAAAIAAAAAQGIBAA":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQAaBQAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgloYWxmIGNvdmVyYWdlOwoJQHN3aXRjaCAoMykgCgl7CgkJY2FzZSAwOiAgICBjYXNlIDI6ICAgICAgICBjb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1N0YWdlMV9jMC54eSwgc2tfRnJhZ0Nvb3JkLnh5KSkpID8gMSA6IDApOwoJCWJyZWFrOwoJCWRlZmF1bHQ6ICAgICAgICBoYWxmNCBkaXN0czQgPSBjbGFtcChoYWxmNCgxLjAsIDEuMCwgLTEuMCwgLTEuMCkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzApLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJQGlmICgzID09IDIgfHwgMyA9PSAzKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIF9pbnB1dCAqIGNvdmVyYWdlOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","KYMAAAAABCYIR6AYYAAAAAAAAAAAAAGIQUKAAAAAABAAKAAAAAQAAAAAAAQSGAAA":"BAAAAExTS1NwAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkRWRnZQoJdlF1YWRFZGdlX1N0YWdlMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAQEAAAAAAAABAQBdBgAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZRdWFkRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQUFSZWN0RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDQgcHJldlJlY3QgPSBmbG9hdDQoLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwKTsKCWhhbGYgY292ZXJhZ2U7CglAc3dpdGNoICgxKSAKCXsKCQljYXNlIDA6ICAgIGNhc2UgMjogICAgICAgIGNvdmVyYWdlID0gaGFsZihhbGwoZ3JlYXRlclRoYW4oZmxvYXQ0KHNrX0ZyYWdDb29yZC54eSwgdXJlY3RVbmlmb3JtX1N0YWdlMV9jMC56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7CgkJYnJlYWs7CgkJZGVmYXVsdDogICAgICAgIGhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1N0YWdlMV9jMCksIDAuMCwgMS4wKTsKCQloYWxmMiBkaXN0czIgPSAoZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3KSAtIDEuMDsKCQljb3ZlcmFnZSA9IGRpc3RzMi54ICogZGlzdHMyLnk7Cgl9CglAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gX2lucHV0ICogY292ZXJhZ2U7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgloYWxmIGVkZ2VBbHBoYTsKCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TdGFnZTAueiA+IDAuMCAmJiB2UXVhZEVkZ2VfU3RhZ2UwLncgPiAwLjApIAoJewoJCWVkZ2VBbHBoYSA9IGhhbGYobWluKG1pbih2UXVhZEVkZ2VfU3RhZ2UwLnosIHZRdWFkRWRnZV9TdGFnZTAudykgKyAwLjUsIDEuMCkpOwoJfQoJZWxzZSAKCXsKCQloYWxmMiBnRiA9IGhhbGYyKGhhbGYoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSksICAgICAgICAgICAgICAgICBoYWxmKDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQUFSZWN0RWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb3ZlcmFnZV9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","IGIAAAAAAIAAAAABCYBR6AAAAAAAAAAAACQAEAAAAAIAAAAAQCIBCAAAAA":"BAAAAExTS1MxAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkCgl2SGFpclF1YWRFZGdlX1N0YWdlMCA9IGluSGFpclF1YWRFZGdlOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAA7AwAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfU3RhZ2UwOwp1bmlmb3JtIGhhbGYgdUNvdmVyYWdlX1N0YWdlMDsKaW4gaGFsZjQgdkhhaXJRdWFkRWRnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJaGFsZiBlZGdlQWxwaGE7CgloYWxmMiBkdXZkeCA9IGhhbGYyKGRGZHgodkhhaXJRdWFkRWRnZV9TdGFnZTAueHkpKTsKCWhhbGYyIGR1dmR5ID0gaGFsZjIoZEZkeSh2SGFpclF1YWRFZGdlX1N0YWdlMC54eSkpOwoJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCWVkZ2VBbHBoYSA9IGhhbGYodkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggLSB2SGFpclF1YWRFZGdlX1N0YWdlMC55KTsKCWVkZ2VBbHBoYSA9IHNxcnQoZWRnZUFscGhhICogZWRnZUFscGhhIC8gZG90KGdGLCBnRikpOwoJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KHVDb3ZlcmFnZV9TdGFnZTAgKiBlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADgAAAGluSGFpclF1YWRFZGdlAAABAAAAAAAAAA==","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAFIQA2AAAAAAQCBAAAAAHIJBAIAAAAAAACMAYIDAAAQCAAAAAAAAKAAQAAAABAAAAACAJAEIAAA":"BAAAAExTS1NpAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMF9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAAAHUEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfU3RhZ2UxX2MwX2MwX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBpbkNvb3JkID0gdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TdGFnZTFfYzBfYzBfYzAueHksIHVjbGFtcF9TdGFnZTFfYzBfYzBfYzAuencpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUgKENvbXBvc2UtT25lIGJlaGF2aW9yKQoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQoMSkpLCBfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IEJsZW5kX1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","AWAAGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAAAUABAAAAACAAAAAEBSAI":"BAAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAACYCAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","B2AAQAAAAQAAAAABC3777777AAOAAAAAAAAAAAAAUABAAAAACAAAAAEASAIQAAAA":"BAAAAExTS1M3AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJdmluQ292ZXJhZ2VfU3RhZ2UwID0gaW5Db3ZlcmFnZTsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAiQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKaW4gaGFsZiB2aW5Db3ZlcmFnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgloYWxmIGFscGhhID0gMS4wOwoJYWxwaGEgPSB2aW5Db3ZlcmFnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChhbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","DIQAAAAAMAAAAABAYAROFA2CAIAAAABAAAAAAAAAAAACAF2SAAAAAAAACUAAAAEAAAAAAAEERQAAA":"BAAAAExTS1NQAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBmbG9hdCB2VGV4SW5kZXhfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNTaXplSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSBmbG9hdCh0ZXhJZHgpOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChpblBvc2l0aW9uLnggLCBpblBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAO0EAAB1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1N0YWdlMDsKaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJaGFsZiBjb3ZlcmFnZTsKCUBzd2l0Y2ggKDEpIAoJewoJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQlicmVhazsKCQlkZWZhdWx0OiAgICAgICAgaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCUBpZiAoMSA9PSAyIHx8IDEgPT0gMykgCgl7CgkJY292ZXJhZ2UgPSAxLjAgLSBjb3ZlcmFnZTsKCX0KCXJldHVybiBfaW5wdXQgKiBjb3ZlcmFnZTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB2VGV4dHVyZUNvb3Jkc19TdGFnZTApLnJycnI7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQUFSZWN0RWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb3ZlcmFnZV9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","GFQQAAAAMAAGGADDACRQAAAAMAACHQDCABRQAI7CAMAAAAAAKQAAAAAAAIAAAAAQGIBAAAA":"BAAAAExTS1PfCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQgYWFfYmxvYXRfbXVsdGlwbGllciA9IDE7CglmbG9hdDIgY29ybmVyID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy54eTsKCWZsb2F0MiByYWRpdXNfb3V0c2V0ID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy56dzsKCWZsb2F0MiBhYV9ibG9hdF9kaXJlY3Rpb24gPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UueHk7CglmbG9hdCBpc19saW5lYXJfY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UudzsKCWZsb2F0MiBwaXhlbGxlbmd0aCA9IGludmVyc2VzcXJ0KGZsb2F0Mihkb3Qoc2tldy54eiwgc2tldy54eiksIGRvdChza2V3Lnl3LCBza2V3Lnl3KSkpOwoJZmxvYXQ0IG5vcm1hbGl6ZWRfYXhpc19kaXJzID0gc2tldyAqIHBpeGVsbGVuZ3RoLnh5eHk7CglmbG9hdDIgYXhpc3dpZHRocyA9IChhYnMobm9ybWFsaXplZF9heGlzX2RpcnMueHkpICsgYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnp3KSk7CglmbG9hdDIgYWFfYmxvYXRyYWRpdXMgPSBheGlzd2lkdGhzICogcGl4ZWxsZW5ndGggKiAuNTsKCWZsb2F0NCByYWRpaV9hbmRfbmVpZ2hib3JzID0gcmFkaWlfc2VsZWN0b3IqIGZsb2F0NHg0KHJhZGlpX3gsIHJhZGlpX3ksIHJhZGlpX3gueXh3eiwgcmFkaWlfeS53enl4KTsKCWZsb2F0MiByYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMueHk7CglmbG9hdDIgbmVpZ2hib3JfcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnp3OwoJZmxvYXQgY292ZXJhZ2VfbXVsdGlwbGllciA9IDE7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2VfbXVsdGlwbGllciA9IDEgLyAobWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpKTsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCX0KCWZsb2F0IGNvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLno7CglpZiAoYW55KGxlc3NUaGFuKHJhZGlpLCBhYV9ibG9hdHJhZGl1cyAqIDEuNSkpKSAKCXsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSBzaWduKGNvcm5lcik7CgkJaWYgKGNvdmVyYWdlID4gLjUpIAoJCXsKCQkJYWFfYmxvYXRfZGlyZWN0aW9uID0gLWFhX2Jsb2F0X2RpcmVjdGlvbjsKCQl9CgkJaXNfbGluZWFyX2NvdmVyYWdlID0gMTsKCX0KCWVsc2UgCgl7CgkJcmFkaWkgPSBjbGFtcChyYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJbmVpZ2hib3JfcmFkaWkgPSBjbGFtcChuZWlnaGJvcl9yYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJZmxvYXQyIHNwYWNpbmcgPSAyIC0gcmFkaWkgLSBuZWlnaGJvcl9yYWRpaTsKCQlmbG9hdDIgZXh0cmFfcGFkID0gbWF4KHBpeGVsbGVuZ3RoICogLjA2MjUgLSBzcGFjaW5nLCBmbG9hdDIoMCkpOwoJCXJhZGlpIC09IGV4dHJhX3BhZCAqIC41OwoJfQoJZmxvYXQyIGFhX291dHNldCA9IGFhX2Jsb2F0X2RpcmVjdGlvbiAqIGFhX2Jsb2F0cmFkaXVzICogYWFfYmxvYXRfbXVsdGlwbGllcjsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglpZiAoY292ZXJhZ2UgPiAuNSkgCgl7CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi54ICE9IDAgJiYgdmVydGV4cG9zLnggKiBjb3JuZXIueCA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueCk7CgkJCXZlcnRleHBvcy54ID0gMDsKCQkJdmVydGV4cG9zLnkgKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLnkpICogcGl4ZWxsZW5ndGgueS9waXhlbGxlbmd0aC54OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueCkgLyAoYWJzKGNvcm5lci54KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueSAhPSAwICYmIHZlcnRleHBvcy55ICogY29ybmVyLnkgPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLnkpOwoJCQl2ZXJ0ZXhwb3MueSA9IDA7CgkJCXZlcnRleHBvcy54ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci54KSAqIHBpeGVsbGVuZ3RoLngvcGl4ZWxsZW5ndGgueTsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLnkpIC8gKGFicyhjb3JuZXIueSkgKyBiYWNrc2V0KSArIC41OwoJCX0KCX0KCWZsb2F0MngyIHNrZXdtYXRyaXggPSBmbG9hdDJ4Mihza2V3Lnh5LCBza2V3Lnp3KTsKCWZsb2F0MiBkZXZjb29yZCA9IHZlcnRleHBvcyAqIHNrZXdtYXRyaXggKyB0cmFuc2xhdGU7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TdGFnZTAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAIcCAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1N0YWdlMC54LCB5PXZhcmNjb29yZF9TdGFnZTAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","AWQAGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAIAXCIAEAAAAAAKQAAAAAAAIAAAAAQGIBAA":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQAaBQAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgloYWxmIGNvdmVyYWdlOwoJQHN3aXRjaCAoMSkgCgl7CgkJY2FzZSAwOiAgICBjYXNlIDI6ICAgICAgICBjb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1N0YWdlMV9jMC54eSwgc2tfRnJhZ0Nvb3JkLnh5KSkpID8gMSA6IDApOwoJCWJyZWFrOwoJCWRlZmF1bHQ6ICAgICAgICBoYWxmNCBkaXN0czQgPSBjbGFtcChoYWxmNCgxLjAsIDEuMCwgLTEuMCwgLTEuMCkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzApLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJQGlmICgxID09IDIgfHwgMSA9PSAzKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIF9pbnB1dCAqIGNvdmVyYWdlOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","K5IAAAAAAAMAAAAAARMPZ72HPQCFR7H7777QGAAAAAAAAAAAIRDQAAAEAAAAAMARAAAAAAAAAAAAAAAAFIAAAAAAAEAAAAAIDEAQAAA":"BAAAAExTS1OpAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAAADmAgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTApLnJycnI7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","K4QACAAAAAGAAAAAAIWP57ZDH37P7777777QCAAAAAAAAAAAMIQHSAAAAAAQAAAAABKAAAAAAABAAAAACAZAEAAAAA":"BAAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAEBAAAAAAAAAQEADAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTFfYzA7CmluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTFfYzAuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxX2MwLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA="}} \ No newline at end of file diff --git a/shaders_2.2.1.sksl.json b/shaders_2.2.1.sksl.json new file mode 100644 index 000000000..cf70aaefc --- /dev/null +++ b/shaders_2.2.1.sksl.json @@ -0,0 +1 @@ +{"platform":"android","name":"SM G970N","engineRevision":"0fdb562ac8068ce3dda6b69aca3f355f4d1d2718","data":{"B2ABSAAAAQAAAAABC3777777AAOAAAAAAAAAAAAAUABAAAAACAAAAAEASAIQAAAA":"BAAAAExTS1OpAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGYgaW5Db3ZlcmFnZTsKb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgY29sb3IgPSB1Q29sb3JfU3RhZ2UwOwoJY29sb3IgPSBjb2xvciAqIGluQ292ZXJhZ2U7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAA6AQAAaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","DISAAAAAMAAAAABAYDRP7H2CAIAAAABAAAAAAABAMQAAAVAAAAAAAAQAAAABAMQC":"BAAAAExTS1MHAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBmbG9hdCB2VGV4SW5kZXhfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNTaXplSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSBmbG9hdCh0ZXhJZHgpOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAADACAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1N0YWdlMDsKZmxhdCBpbiBmbG9hdCB2VGV4SW5kZXhfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCWhhbGY0IHRleENvbG9yOwoJewoJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKTsKCX0KCW91dHB1dENvbG9yX1N0YWdlMCA9IG91dHB1dENvbG9yX1N0YWdlMCAqIHRleENvbG9yOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","GFQQAAAAMAAGGADDACRQAAAAMAACHQDCABRQAI7CAMAAAABAC5JAAAAAAAABKAAAACAAAAAAACCIYAAAAAAA":"BAAAAExTS1PfCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQgYWFfYmxvYXRfbXVsdGlwbGllciA9IDE7CglmbG9hdDIgY29ybmVyID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy54eTsKCWZsb2F0MiByYWRpdXNfb3V0c2V0ID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy56dzsKCWZsb2F0MiBhYV9ibG9hdF9kaXJlY3Rpb24gPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UueHk7CglmbG9hdCBpc19saW5lYXJfY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UudzsKCWZsb2F0MiBwaXhlbGxlbmd0aCA9IGludmVyc2VzcXJ0KGZsb2F0Mihkb3Qoc2tldy54eiwgc2tldy54eiksIGRvdChza2V3Lnl3LCBza2V3Lnl3KSkpOwoJZmxvYXQ0IG5vcm1hbGl6ZWRfYXhpc19kaXJzID0gc2tldyAqIHBpeGVsbGVuZ3RoLnh5eHk7CglmbG9hdDIgYXhpc3dpZHRocyA9IChhYnMobm9ybWFsaXplZF9heGlzX2RpcnMueHkpICsgYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnp3KSk7CglmbG9hdDIgYWFfYmxvYXRyYWRpdXMgPSBheGlzd2lkdGhzICogcGl4ZWxsZW5ndGggKiAuNTsKCWZsb2F0NCByYWRpaV9hbmRfbmVpZ2hib3JzID0gcmFkaWlfc2VsZWN0b3IqIGZsb2F0NHg0KHJhZGlpX3gsIHJhZGlpX3ksIHJhZGlpX3gueXh3eiwgcmFkaWlfeS53enl4KTsKCWZsb2F0MiByYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMueHk7CglmbG9hdDIgbmVpZ2hib3JfcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnp3OwoJZmxvYXQgY292ZXJhZ2VfbXVsdGlwbGllciA9IDE7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2VfbXVsdGlwbGllciA9IDEgLyAobWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpKTsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCX0KCWZsb2F0IGNvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLno7CglpZiAoYW55KGxlc3NUaGFuKHJhZGlpLCBhYV9ibG9hdHJhZGl1cyAqIDEuNSkpKSAKCXsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSBzaWduKGNvcm5lcik7CgkJaWYgKGNvdmVyYWdlID4gLjUpIAoJCXsKCQkJYWFfYmxvYXRfZGlyZWN0aW9uID0gLWFhX2Jsb2F0X2RpcmVjdGlvbjsKCQl9CgkJaXNfbGluZWFyX2NvdmVyYWdlID0gMTsKCX0KCWVsc2UgCgl7CgkJcmFkaWkgPSBjbGFtcChyYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJbmVpZ2hib3JfcmFkaWkgPSBjbGFtcChuZWlnaGJvcl9yYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJZmxvYXQyIHNwYWNpbmcgPSAyIC0gcmFkaWkgLSBuZWlnaGJvcl9yYWRpaTsKCQlmbG9hdDIgZXh0cmFfcGFkID0gbWF4KHBpeGVsbGVuZ3RoICogLjA2MjUgLSBzcGFjaW5nLCBmbG9hdDIoMCkpOwoJCXJhZGlpIC09IGV4dHJhX3BhZCAqIC41OwoJfQoJZmxvYXQyIGFhX291dHNldCA9IGFhX2Jsb2F0X2RpcmVjdGlvbiAqIGFhX2Jsb2F0cmFkaXVzICogYWFfYmxvYXRfbXVsdGlwbGllcjsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglpZiAoY292ZXJhZ2UgPiAuNSkgCgl7CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi54ICE9IDAgJiYgdmVydGV4cG9zLnggKiBjb3JuZXIueCA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueCk7CgkJCXZlcnRleHBvcy54ID0gMDsKCQkJdmVydGV4cG9zLnkgKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLnkpICogcGl4ZWxsZW5ndGgueS9waXhlbGxlbmd0aC54OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueCkgLyAoYWJzKGNvcm5lci54KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueSAhPSAwICYmIHZlcnRleHBvcy55ICogY29ybmVyLnkgPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLnkpOwoJCQl2ZXJ0ZXhwb3MueSA9IDA7CgkJCXZlcnRleHBvcy54ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci54KSAqIHBpeGVsbGVuZ3RoLngvcGl4ZWxsZW5ndGgueTsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLnkpIC8gKGFicyhjb3JuZXIueSkgKyBiYWNrc2V0KSArIC41OwoJCX0KCX0KCWZsb2F0MngyIHNrZXdtYXRyaXggPSBmbG9hdDJ4Mihza2V3Lnh5LCBza2V3Lnp3KTsKCWZsb2F0MiBkZXZjb29yZCA9IHZlcnRleHBvcyAqIHNrZXdtYXRyaXggKyB0cmFuc2xhdGU7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TdGFnZTAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAABAQAAAAAAAAEBAHsFAAB1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJaGFsZiBjb3ZlcmFnZTsKCUBzd2l0Y2ggKDEpIAoJewoJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQlicmVhazsKCQlkZWZhdWx0OiAgICAgICAgaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCUBpZiAoMSA9PSAyIHx8IDEgPT0gMykgCgl7CgkJY292ZXJhZ2UgPSAxLjAgLSBjb3ZlcmFnZTsKCX0KCXJldHVybiBfaW5wdXQgKiBjb3ZlcmFnZTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1N0YWdlMC54LCB5PXZhcmNjb29yZF9TdGFnZTAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","KYMAAAAABCYIR6AYYAAAAAAAAAAAAAAACUAAAAEAAAAAAAEERQAA":"BAAAAExTS1NwAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkRWRnZQoJdlF1YWRFZGdlX1N0YWdlMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAABpAwAAaW4gZmxvYXQ0IHZRdWFkRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgloYWxmIGVkZ2VBbHBoYTsKCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TdGFnZTAueiA+IDAuMCAmJiB2UXVhZEVkZ2VfU3RhZ2UwLncgPiAwLjApIAoJewoJCWVkZ2VBbHBoYSA9IGhhbGYobWluKG1pbih2UXVhZEVkZ2VfU3RhZ2UwLnosIHZRdWFkRWRnZV9TdGFnZTAudykgKyAwLjUsIDEuMCkpOwoJfQoJZWxzZSAKCXsKCQloYWxmMiBnRiA9IGhhbGYyKGhhbGYoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSksICAgICAgICAgICAgICAgICBoYWxmKDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","K4JAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAABAAAAAABBAMADCEB4QAAAAAEAAAAAAKQAAAAAAAIAAAAAQGIBAAAAA":"BAAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEApwMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTFfYzAuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTFfYzAueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdGV4Q29vcmQpICogaGFsZjQoMSkpKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBDaXJjdWxhclJSZWN0X1N0YWdlMV9jMChvdXRwdXRDb3ZlcmFnZV9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AWQAGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAIAXGIAEAAAAAAKQAAAAAAAIAAAAAQGIBAA":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQAaBQAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgloYWxmIGNvdmVyYWdlOwoJQHN3aXRjaCAoMykgCgl7CgkJY2FzZSAwOiAgICBjYXNlIDI6ICAgICAgICBjb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1N0YWdlMV9jMC54eSwgc2tfRnJhZ0Nvb3JkLnh5KSkpID8gMSA6IDApOwoJCWJyZWFrOwoJCWRlZmF1bHQ6ICAgICAgICBoYWxmNCBkaXN0czQgPSBjbGFtcChoYWxmNCgxLjAsIDEuMCwgLTEuMCwgLTEuMCkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzApLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJQGlmICgzID09IDIgfHwgMyA9PSAzKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIF9pbnB1dCAqIGNvdmVyYWdlOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQYAAAAAAQAAQAAAAGJCABAAACAACAAAAABAGOAQAAQAAAABQCVAEQAGAAAAAAAAAAAVAAAAAAAAQAAAABAMQCAAAAA":"BAAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAcFAAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs3XTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMF9jMDsKdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMF9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBjbGFtcChzdWJzZXRDb29yZC54LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwX2MwX2MwLngsIHVjbGFtcF9TdGFnZTFfYzBfYzBfYzBfYzAueik7CgljbGFtcGVkQ29vcmQueSA9IHN1YnNldENvb3JkLnk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzBfYzAoX2lucHV0LCAoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwX2MwKSAqIF9jb29yZHMueHkxKS54eSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgXzJfY29sb3IgPSBoYWxmNCgwLjApOwoJZmxvYXQyIF8zX2Nvb3JkID0gKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCAtIGZsb2F0MigoMTIuMCAqIHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNl0ueCkpOwoJcmV0dXJuIF8yX2NvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fU3RhZ2UxX2MwX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","K5JAAAAAAAMAAAAAARMPZ72HPQCFR7H7777QGAAAAACAAAAAACCAYAEABIAAAACAAAAAAACCIYAAA":"BAAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAAcAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfU3RhZ2UwKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","GFQQAAAAMAAGGADDACRQAAAAMAACHQDCABRQAI7CAMAAAAAAKQAAAAAAAIAAAAAQGIBAAAA":"BAAAAExTS1PfCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQgYWFfYmxvYXRfbXVsdGlwbGllciA9IDE7CglmbG9hdDIgY29ybmVyID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy54eTsKCWZsb2F0MiByYWRpdXNfb3V0c2V0ID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy56dzsKCWZsb2F0MiBhYV9ibG9hdF9kaXJlY3Rpb24gPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UueHk7CglmbG9hdCBpc19saW5lYXJfY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UudzsKCWZsb2F0MiBwaXhlbGxlbmd0aCA9IGludmVyc2VzcXJ0KGZsb2F0Mihkb3Qoc2tldy54eiwgc2tldy54eiksIGRvdChza2V3Lnl3LCBza2V3Lnl3KSkpOwoJZmxvYXQ0IG5vcm1hbGl6ZWRfYXhpc19kaXJzID0gc2tldyAqIHBpeGVsbGVuZ3RoLnh5eHk7CglmbG9hdDIgYXhpc3dpZHRocyA9IChhYnMobm9ybWFsaXplZF9heGlzX2RpcnMueHkpICsgYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnp3KSk7CglmbG9hdDIgYWFfYmxvYXRyYWRpdXMgPSBheGlzd2lkdGhzICogcGl4ZWxsZW5ndGggKiAuNTsKCWZsb2F0NCByYWRpaV9hbmRfbmVpZ2hib3JzID0gcmFkaWlfc2VsZWN0b3IqIGZsb2F0NHg0KHJhZGlpX3gsIHJhZGlpX3ksIHJhZGlpX3gueXh3eiwgcmFkaWlfeS53enl4KTsKCWZsb2F0MiByYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMueHk7CglmbG9hdDIgbmVpZ2hib3JfcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnp3OwoJZmxvYXQgY292ZXJhZ2VfbXVsdGlwbGllciA9IDE7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2VfbXVsdGlwbGllciA9IDEgLyAobWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpKTsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCX0KCWZsb2F0IGNvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLno7CglpZiAoYW55KGxlc3NUaGFuKHJhZGlpLCBhYV9ibG9hdHJhZGl1cyAqIDEuNSkpKSAKCXsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSBzaWduKGNvcm5lcik7CgkJaWYgKGNvdmVyYWdlID4gLjUpIAoJCXsKCQkJYWFfYmxvYXRfZGlyZWN0aW9uID0gLWFhX2Jsb2F0X2RpcmVjdGlvbjsKCQl9CgkJaXNfbGluZWFyX2NvdmVyYWdlID0gMTsKCX0KCWVsc2UgCgl7CgkJcmFkaWkgPSBjbGFtcChyYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJbmVpZ2hib3JfcmFkaWkgPSBjbGFtcChuZWlnaGJvcl9yYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJZmxvYXQyIHNwYWNpbmcgPSAyIC0gcmFkaWkgLSBuZWlnaGJvcl9yYWRpaTsKCQlmbG9hdDIgZXh0cmFfcGFkID0gbWF4KHBpeGVsbGVuZ3RoICogLjA2MjUgLSBzcGFjaW5nLCBmbG9hdDIoMCkpOwoJCXJhZGlpIC09IGV4dHJhX3BhZCAqIC41OwoJfQoJZmxvYXQyIGFhX291dHNldCA9IGFhX2Jsb2F0X2RpcmVjdGlvbiAqIGFhX2Jsb2F0cmFkaXVzICogYWFfYmxvYXRfbXVsdGlwbGllcjsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglpZiAoY292ZXJhZ2UgPiAuNSkgCgl7CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi54ICE9IDAgJiYgdmVydGV4cG9zLnggKiBjb3JuZXIueCA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueCk7CgkJCXZlcnRleHBvcy54ID0gMDsKCQkJdmVydGV4cG9zLnkgKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLnkpICogcGl4ZWxsZW5ndGgueS9waXhlbGxlbmd0aC54OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueCkgLyAoYWJzKGNvcm5lci54KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueSAhPSAwICYmIHZlcnRleHBvcy55ICogY29ybmVyLnkgPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLnkpOwoJCQl2ZXJ0ZXhwb3MueSA9IDA7CgkJCXZlcnRleHBvcy54ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci54KSAqIHBpeGVsbGVuZ3RoLngvcGl4ZWxsZW5ndGgueTsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLnkpIC8gKGFicyhjb3JuZXIueSkgKyBiYWNrc2V0KSArIC41OwoJCX0KCX0KCWZsb2F0MngyIHNrZXdtYXRyaXggPSBmbG9hdDJ4Mihza2V3Lnh5LCBza2V3Lnp3KTsKCWZsb2F0MiBkZXZjb29yZCA9IHZlcnRleHBvcyAqIHNrZXdtYXRyaXggKyB0cmFuc2xhdGU7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TdGFnZTAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAIcCAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1N0YWdlMC54LCB5PXZhcmNjb29yZF9TdGFnZTAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","KYMAAAAABCYIR6AYYAAAAAAAAAAAAAEIQHSACAAAAQAAAAAAKAAQAAAABAAAAACAZAEAAAA":"BAAAAExTS1NwAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkRWRnZQoJdlF1YWRFZGdlX1N0YWdlMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAQEAAAAAAAABAQA1BQAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZRdWFkRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTFfYzAuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTFfYzAueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gX2lucHV0ICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgloYWxmIGVkZ2VBbHBoYTsKCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TdGFnZTAueiA+IDAuMCAmJiB2UXVhZEVkZ2VfU3RhZ2UwLncgPiAwLjApIAoJewoJCWVkZ2VBbHBoYSA9IGhhbGYobWluKG1pbih2UXVhZEVkZ2VfU3RhZ2UwLnosIHZRdWFkRWRnZV9TdGFnZTAudykgKyAwLjUsIDEuMCkpOwoJfQoJZWxzZSAKCXsKCQloYWxmMiBnRiA9IGhhbGYyKGhhbGYoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSksICAgICAgICAgICAgICAgICBoYWxmKDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpblF1YWRFZGdlAAABAAAAAAAAAA==","IYBQAAAAAELBCHYCDYAAAAAAAEAAAACAIQAABIACAAAAAEAAAAAIBEARAA":"BAAAAExTS1OFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmMyBpblNoYWRvd1BhcmFtczsKb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFJSZWN0U2hhZG93Cgl2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwID0gaW5TaGFkb3dQYXJhbXM7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAABPAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBoYWxmMyB2aW5TaGFkb3dQYXJhbXNfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBSUmVjdFNoYWRvdwoJaGFsZjMgc2hhZG93UGFyYW1zOwoJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCWhhbGYgZCA9IGxlbmd0aChzaGFkb3dQYXJhbXMueHkpOwoJZmxvYXQyIHV2ID0gZmxvYXQyKHNoYWRvd1BhcmFtcy56ICogKDEuMCAtIGQpLCAwLjUpOwoJaGFsZiBmYWN0b3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB1dikuMDAwci5hOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZmFjdG9yKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA4AAABpblNoYWRvd1BhcmFtcwAAAQAAAAAAAAA=","KYNQAAAABCYIR6AYYAAAAAAAAAAAAADQR4AOAAPAAHQADYBRAAAAAACECQAAAABYDIAAAAAADUEQAAAAWAQQAAAAAAB4SAYAAAEAAAAAACQAEAAAAAIAAAAAQCIBCAAA":"BAAAAExTS1OgAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TdGFnZTAgPSBpblF1YWRFZGdlOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAEBAAAAAAAAAQEA5AoAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UyX2MxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTJfYzE7CmluIGZsb2F0NCB2UXVhZEVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gaGFsZjQoaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2LCAxLjAsIDAuMCwgMC4wKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBMaW5lYXJHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIG1peCh1c3RhcnRfU3RhZ2UxX2MwX2MwX2MxLCB1ZW5kX1N0YWdlMV9jMF9jMF9jMSwgaGFsZihfY29vcmRzLngpKTsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMF9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJb3V0Q29sb3IgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglAaWYgKGZhbHNlKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBvdXRDb2xvcjsKfQpoYWxmNCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3JfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwX2MwKGZhbHNlID8gaGFsZjQoMCkgOiBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCkpOwp9CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UyX2MxKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTJfYzEuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UyX2MxLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UyX2MxLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkRWRnZQoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJaGFsZiBlZGdlQWxwaGE7CgloYWxmMiBkdXZkeCA9IGhhbGYyKGRGZHgodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCWlmICh2UXVhZEVkZ2VfU3RhZ2UwLnogPiAwLjAgJiYgdlF1YWRFZGdlX1N0YWdlMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1N0YWdlMC56LCB2UXVhZEVkZ2VfU3RhZ2UwLncpICsgMC41LCAxLjApKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjIgZ0YgPSBoYWxmMihoYWxmKDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR5LnggLSBkdXZkeS55KSk7CgkJZWRnZUFscGhhID0gaGFsZih2UXVhZEVkZ2VfU3RhZ2UwLngqdlF1YWRFZGdlX1N0YWdlMC54IC0gdlF1YWRFZGdlX1N0YWdlMC55KTsKCQllZGdlQWxwaGEgPSBzYXR1cmF0ZSgwLjUgLSBlZGdlQWxwaGEgLyBsZW5ndGgoZ0YpKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3Nvcl9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwKTsKCWhhbGY0IG91dHB1dF9TdGFnZTI7CglvdXRwdXRfU3RhZ2UyID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTJfYzEob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0X1N0YWdlMjsKCX0KfQoAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAKAAAAaW5RdWFkRWRnZQAAAQAAAAAAAAA=","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAA3QRQA6AAPAAHQADYBQAAAAAACECQAAAARYDAAAAAIBDQEAAAABWAQAAAABAEOAAAAQAAAABQCEAAAAAAAAAAAAAAAAVAAAAAAAAQAAAABAMQCAAA":"BAAAAExTS1MOAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTJfYzE7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18xX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzBfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMV9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMl9jMSkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAAXggAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzBfYzE7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTJfYzE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMjsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18xX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBoYWxmNChoYWxmKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMC54KSArIDkuOTk5OTk5NzQ3Mzc4NzUxNmUtMDYsIDEuMCwgMC4wLCAwLjApOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IFNpbmdsZUludGVydmFsR3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MwX2MxKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gbWl4KHVzdGFydF9TdGFnZTFfYzBfYzBfYzEsIHVlbmRfU3RhZ2UxX2MwX2MwX2MxLCBoYWxmKF9jb29yZHMueCkpOwp9CmhhbGY0IENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKCWhhbGY0IG91dENvbG9yOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSBoYWxmNCgwLjApOwoJfQoJZWxzZSBpZiAodC54IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlvdXRDb2xvciA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMF9jMDsKCX0KCWVsc2UgCgl7CgkJb3V0Q29sb3IgPSBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMF9jMShfaW5wdXQsIGZsb2F0MihoYWxmMih0LngsIDAuMCkpKTsKCX0KCUBpZiAoZmFsc2UpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIG91dENvbG9yOwp9CmhhbGY0IE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3Nvcl9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzBfYzAoZmFsc2UgPyBoYWxmNCgwKSA6IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKSk7Cn0KaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTJfYzFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTIsIHZUcmFuc2Zvcm1lZENvb3Jkc18xX1N0YWdlMCkucnJycjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UyX2MxKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1N0YWdlMl9jMV9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3Nvcl9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwKTsKCWhhbGY0IG91dHB1dF9TdGFnZTI7CglvdXRwdXRfU3RhZ2UyID0gTWF0cml4RWZmZWN0X1N0YWdlMl9jMShvdXRwdXRDb3ZlcmFnZV9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRfU3RhZ2UyOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","K4QAAAAAAAGAAAAAAIWP57ZDH37P7777777QCAAAAAAAAAAAIACQAAAAEAAAAAAAEERQAAA":"BAAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAEUBAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","AWQAGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAIAGGIDYAAAQAAAAAAAAAVAAAABAAAAAAABBEMAAAA":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQAIBAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTFfYzAuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxX2MwLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwLnggLSBsZW5ndGgoZHh5KSkpOwoJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0NCBjaXJjbGVFZGdlOwoJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBDaXJjdWxhclJSZWN0X1N0YWdlMV9jMChvdXRwdXRDb3ZlcmFnZV9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQAAICAAAAA5AIEBAAAAAAAAJQDBAMAAAAIAAAAAAKAAQAAAABAAAAACAJACIAAAAA":"BAAAAExTS1OjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMF9jMCkgKiAodW1hdHJpeF9TdGFnZTFfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAgAQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TdGFnZTFfYzBfYzBfYzAueSwgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMC53KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AWQAGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAAAGOIDYAAAQAAAAAABAEADAAAAAAAAAIAAAAAIAIQAAABAAAAAOQDAAQAAAAAQCUYBCAAAAAAAAAAAAAAAUABAAAAACAAAAAEBSAIAAAA":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQDiBgAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMl9jMV9jMF9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UyOwppbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTFfYzAuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTFfYzAueCAtIGxlbmd0aChkeHkpKSk7CglhbHBoYSA9IDEuMCAtIGFscGhhOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UyX2MxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMiwgX2Nvb3JkcykuMDAwcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UyX2MxX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TdGFnZTJfYzFfYzBfYzBfYzAoX2lucHV0LCAoKHVtYXRyaXhfU3RhZ2UyX2MxX2MwX2MwKSAqIF9jb29yZHMueHkxKS54eSk7Cn0KaGFsZjQgRGV2aWNlU3BhY2VFZmZlY3RfU3RhZ2UyX2MxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBNYXRyaXhFZmZlY3RfU3RhZ2UyX2MxX2MwX2MwKF9pbnB1dCwgc2tfRnJhZ0Nvb3JkLnh5KTsKfQpoYWxmNCBCbGVuZF9TdGFnZTJfYzEoaGFsZjQgX2lucHV0KSAKewoJLy8gQmxlbmQgbW9kZTogRHN0SW4gKENvbXBvc2UtT25lIGJlaGF2aW9yKQoJcmV0dXJuIGJsZW5kX2RzdF9pbihEZXZpY2VTcGFjZUVmZmVjdF9TdGFnZTJfYzFfYzAoaGFsZjQoMSkpLCBfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7CgloYWxmNCBvdXRwdXRfU3RhZ2UyOwoJb3V0cHV0X1N0YWdlMiA9IEJsZW5kX1N0YWdlMl9jMShvdXRwdXRfU3RhZ2UxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UyOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","AWQAGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAAAUABAAAAACAAAAAEBSAI":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAAmAgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQCAICAAAABGAIEBSAAIBAAAAAAAAACUAAAAEAAAAAAAEERQAAAAA":"BAAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAADJAwAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfU3RhZ2UxX2MwX2MwLnh5LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","B2ARQAAAAQAAAAABC3777777AAOAAAAAAAAAAAAA5UIQAPAAHQADYAB4AYAAAAEARABAAAAAI4BQAAAAUARQCAAAAA3AIAAAABAAKAAAAAQAAAAAAAQSGAAAAA":"BAAAAExTS1NnAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAADgHAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzBfYzE7CmluIGhhbGYgdmluQ292ZXJhZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBMaW5lYXJHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIGhhbGY0KGhhbGYodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzBfYzEsIGhhbGYoX2Nvb3Jkcy54KSk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCW91dENvbG9yID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsR3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMC4wKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJb3V0Q29sb3IueHl6ICo9IG91dENvbG9yLnc7Cgl9CglyZXR1cm4gb3V0Q29sb3I7Cn0KaGFsZjQgT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMF9jMChmYWxzZSA/IGhhbGY0KDApIDogaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApKTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoYWxwaGEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3JfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","K5IAAAAAAAMAAAAAARMPZ72HPQCFR7H7777QGAAAAAAAAAAAIRDQAAAEAAAAAMARAAAAAAAAAAAAAAAAFIAAAAAAAEAAAAAIDEAQAAA":"BAAAAExTS1OpAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAAADmAgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTApLnJycnI7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","B2ARQAAAAQAAAAABC3777777AAOAAAAAAAAAAAAA5YIQAPAAHQADYAB4AYAAAAEARABAAAAAI4BQAAAAUARQCAAAAA3AIAAAABQCA6IAAAAACAAAAAAFIAAAAAAAEAAAAAIDEAQ":"BAAAAExTS1NnAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXZpbkNvdmVyYWdlX1N0YWdlMCA9IGluQ292ZXJhZ2U7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAABAQAAAAAAAAEBAAQJAAB1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzBfYzE7CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UyX2MxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTJfYzE7CmluIGhhbGYgdmluQ292ZXJhZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBMaW5lYXJHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIGhhbGY0KGhhbGYodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzBfYzEsIGhhbGYoX2Nvb3Jkcy54KSk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCW91dENvbG9yID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsR3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMC4wKSkpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJb3V0Q29sb3IueHl6ICo9IG91dENvbG9yLnc7Cgl9CglyZXR1cm4gb3V0Q29sb3I7Cn0KaGFsZjQgT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMF9jMChmYWxzZSA/IGhhbGY0KDApIDogaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApKTsKfQpoYWxmNCBDaXJjdWxhclJSZWN0X1N0YWdlMl9jMShoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UyX2MxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMl9jMS5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMl9jMS54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJaGFsZiBhbHBoYSA9IDEuMDsKCWFscGhhID0gdmluQ292ZXJhZ2VfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoYWxwaGEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3JfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCk7CgloYWxmNCBvdXRwdXRfU3RhZ2UyOwoJb3V0cHV0X1N0YWdlMiA9IENpcmN1bGFyUlJlY3RfU3RhZ2UyX2MxKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dF9TdGFnZTI7Cgl9Cn0KAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAACgAAAGluQ292ZXJhZ2UAAAEAAAAAAAAA","GFQAAAAAMAAGGADDACRQAAAAMAACHQDCABRQAI7CAMAAAABAC5JAAAAAAAABKAAAACAAAAAAACCIYAAAAAAA":"BAAAAExTS1NJDAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQgYWFfYmxvYXRfbXVsdGlwbGllciA9IDE7CglmbG9hdDIgY29ybmVyID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy54eTsKCWZsb2F0MiByYWRpdXNfb3V0c2V0ID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy56dzsKCWZsb2F0MiBhYV9ibG9hdF9kaXJlY3Rpb24gPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UueHk7CglmbG9hdCBpc19saW5lYXJfY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UudzsKCWZsb2F0MiBwaXhlbGxlbmd0aCA9IGludmVyc2VzcXJ0KGZsb2F0Mihkb3Qoc2tldy54eiwgc2tldy54eiksIGRvdChza2V3Lnl3LCBza2V3Lnl3KSkpOwoJZmxvYXQ0IG5vcm1hbGl6ZWRfYXhpc19kaXJzID0gc2tldyAqIHBpeGVsbGVuZ3RoLnh5eHk7CglmbG9hdDIgYXhpc3dpZHRocyA9IChhYnMobm9ybWFsaXplZF9heGlzX2RpcnMueHkpICsgYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnp3KSk7CglmbG9hdDIgYWFfYmxvYXRyYWRpdXMgPSBheGlzd2lkdGhzICogcGl4ZWxsZW5ndGggKiAuNTsKCWZsb2F0NCByYWRpaV9hbmRfbmVpZ2hib3JzID0gcmFkaWlfc2VsZWN0b3IqIGZsb2F0NHg0KHJhZGlpX3gsIHJhZGlpX3ksIHJhZGlpX3gueXh3eiwgcmFkaWlfeS53enl4KTsKCWZsb2F0MiByYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMueHk7CglmbG9hdDIgbmVpZ2hib3JfcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnp3OwoJZmxvYXQgY292ZXJhZ2VfbXVsdGlwbGllciA9IDE7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2VfbXVsdGlwbGllciA9IDEgLyAobWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpKTsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCX0KCWZsb2F0IGNvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLno7CglpZiAoYW55KGxlc3NUaGFuKHJhZGlpLCBhYV9ibG9hdHJhZGl1cyAqIDEuNSkpKSAKCXsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSBzaWduKGNvcm5lcik7CgkJaWYgKGNvdmVyYWdlID4gLjUpIAoJCXsKCQkJYWFfYmxvYXRfZGlyZWN0aW9uID0gLWFhX2Jsb2F0X2RpcmVjdGlvbjsKCQl9CgkJaXNfbGluZWFyX2NvdmVyYWdlID0gMTsKCX0KCWVsc2UgCgl7CgkJcmFkaWkgPSBjbGFtcChyYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJbmVpZ2hib3JfcmFkaWkgPSBjbGFtcChuZWlnaGJvcl9yYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJZmxvYXQyIHNwYWNpbmcgPSAyIC0gcmFkaWkgLSBuZWlnaGJvcl9yYWRpaTsKCQlmbG9hdDIgZXh0cmFfcGFkID0gbWF4KHBpeGVsbGVuZ3RoICogLjA2MjUgLSBzcGFjaW5nLCBmbG9hdDIoMCkpOwoJCXJhZGlpIC09IGV4dHJhX3BhZCAqIC41OwoJfQoJZmxvYXQyIGFhX291dHNldCA9IGFhX2Jsb2F0X2RpcmVjdGlvbiAqIGFhX2Jsb2F0cmFkaXVzICogYWFfYmxvYXRfbXVsdGlwbGllcjsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglpZiAoY292ZXJhZ2UgPiAuNSkgCgl7CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi54ICE9IDAgJiYgdmVydGV4cG9zLnggKiBjb3JuZXIueCA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueCk7CgkJCXZlcnRleHBvcy54ID0gMDsKCQkJdmVydGV4cG9zLnkgKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLnkpICogcGl4ZWxsZW5ndGgueS9waXhlbGxlbmd0aC54OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueCkgLyAoYWJzKGNvcm5lci54KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueSAhPSAwICYmIHZlcnRleHBvcy55ICogY29ybmVyLnkgPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLnkpOwoJCQl2ZXJ0ZXhwb3MueSA9IDA7CgkJCXZlcnRleHBvcy54ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci54KSAqIHBpeGVsbGVuZ3RoLngvcGl4ZWxsZW5ndGgueTsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLnkpIC8gKGFicyhjb3JuZXIueSkgKyBiYWNrc2V0KSArIC41OwoJCX0KCX0KCWZsb2F0MngyIHNrZXdtYXRyaXggPSBmbG9hdDJ4Mihza2V3Lnh5LCBza2V3Lnp3KTsKCWZsb2F0MiBkZXZjb29yZCA9IHZlcnRleHBvcyAqIHNrZXdtYXRyaXggKyB0cmFuc2xhdGU7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TdGFnZTAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqIChhcmNjb29yZC9yYWRpaSAqIDIpOwoJfQoJc2tfUG9zaXRpb24gPSBmbG9hdDQoZGV2Y29vcmQueCAsIGRldmNvb3JkLnksIDAsIDEpOwp9CgAAAAABAQAAAAAAAAEBALgFAAB1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0NCB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJaGFsZiBjb3ZlcmFnZTsKCUBzd2l0Y2ggKDEpIAoJewoJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQlicmVhazsKCQlkZWZhdWx0OiAgICAgICAgaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCUBpZiAoMSA9PSAyIHx8IDEgPT0gMykgCgl7CgkJY292ZXJhZ2UgPSAxLjAgLSBjb3ZlcmFnZTsKCX0KCXJldHVybiBfaW5wdXQgKiBjb3ZlcmFnZTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1N0YWdlMC54LCB5PXZhcmNjb29yZF9TdGFnZTAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZ3g9dmFyY2Nvb3JkX1N0YWdlMC56LCBneT12YXJjY29vcmRfU3RhZ2UwLnc7CgkJZmxvYXQgZm53aWR0aCA9IGFicyhneCkgKyBhYnMoZ3kpOwoJCWNvdmVyYWdlID0gLjUgLSBoYWxmKGZuL2Zud2lkdGgpOwoJCWNvdmVyYWdlID0gY2xhbXAoY292ZXJhZ2UsIDAsIDEpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","DIQAAAAAMAAAAABAYAROFA2CAIAAAABAAAAAAAAAAAAAAVAAAAAAAAQAAAABAMQC":"BAAAAExTS1NQAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBmbG9hdCB2VGV4SW5kZXhfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNTaXplSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSBmbG9hdCh0ZXhJZHgpOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChpblBvc2l0aW9uLnggLCBpblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAPkBAAB1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1N0YWdlMDsKaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB2VGV4dHVyZUNvb3Jkc19TdGFnZTApLnJycnI7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAFIQA2AAAAAAAABAAAAAHIBAAIAAAAAAACMAYIDAAAAAAAAAAAAAKAAQAAAABAAAAACAJAEIAAA":"BAAAAExTS1NpAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMF9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAAAE0DAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTApOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgQmxlbmRfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCS8vIEJsZW5kIG1vZGU6IE1vZHVsYXRlIChDb21wb3NlLU9uZSBiZWhhdmlvcikKCXJldHVybiBibGVuZF9tb2R1bGF0ZShNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0KDEpKSwgX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBCbGVuZF9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","K4QACAAAAAGAAAAAAIWP57ZDH37P7777777QCAAAAAAAAAAAIACQAAAAEAAAAAAAEERQAAA":"BAAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAQAEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","AWQQGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAIAXCIAEAAAAAAKQAAAAAAAIAAAAAQGIBAA":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQCrBQAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgloYWxmIGNvdmVyYWdlOwoJQHN3aXRjaCAoMSkgCgl7CgkJY2FzZSAwOiAgICBjYXNlIDI6ICAgICAgICBjb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1N0YWdlMV9jMC54eSwgc2tfRnJhZ0Nvb3JkLnh5KSkpID8gMSA6IDApOwoJCWJyZWFrOwoJCWRlZmF1bHQ6ICAgICAgICBoYWxmNCBkaXN0czQgPSBjbGFtcChoYWxmNCgxLjAsIDEuMCwgLTEuMCwgLTEuMCkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzApLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJQGlmICgxID09IDIgfHwgMSA9PSAzKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIF9pbnB1dCAqIGNvdmVyYWdlOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAFIQA2AAAAAAQCBAAAAAHIJBAIAAAAAAACMAYIDAAAQCAAAAAAAAKAAQAAAABAAAAACAJAEIAAA":"BAAAAExTS1NpAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMF9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAAAHUEAAB1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfU3RhZ2UxX2MwX2MwX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBpbkNvb3JkID0gdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkID0gY2xhbXAoc3Vic2V0Q29vcmQsIHVjbGFtcF9TdGFnZTFfYzBfYzBfYzAueHksIHVjbGFtcF9TdGFnZTFfYzBfYzBfYzAuencpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBCbGVuZF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJLy8gQmxlbmQgbW9kZTogTW9kdWxhdGUgKENvbXBvc2UtT25lIGJlaGF2aW9yKQoJcmV0dXJuIGJsZW5kX21vZHVsYXRlKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQoMSkpLCBfaW5wdXQpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IEJsZW5kX1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","AWQAGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAIAGCIDYAAAQAAAAAAAAAVAAAABAAAAAAABBEMAAAA":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQDyAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTFfYzAuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxX2MwLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","K4QACAAAAAGAAAAAAIWP57ZDH37P7777777QCAAAAAAAAAAAMIQHSAAAAAAQAAAAABKAAAAAAABAAAAACAZAEAAAAA":"BAAAAExTS1PvAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAEBAAAAAAAAAQEADAMAAHVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxX2MwOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTFfYzA7CmluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTFfYzAuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxX2MwLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQYAAAAAAAAAQAAAAGJCABAAAAAACAAAAABAGOAAAAQAAAABQCVAEQAEAAAAAAAAAAAVAAAAAAAAQAAAABAMQCAAAAA":"BAAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAADIEgAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs3XTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMF9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIF9jb29yZHMpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMF9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzBfYzBfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKfQpoYWxmNCBHYXVzc2lhbkNvbnZvbHV0aW9uX1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfMl9jb2xvciA9IGhhbGY0KDAuMCk7CglmbG9hdDIgXzNfY29vcmQgPSAodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwIC0gZmxvYXQyKCgxMi4wICogdV8wX0luY3JlbWVudF9TdGFnZTFfYzBfYzApKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFswXS54KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFswXS55KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFswXS56KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFswXS53KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsxXS54KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsxXS55KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsxXS56KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsxXS53KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsyXS54KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsyXS55KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsyXS56KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFsyXS53KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFszXS54KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFszXS55KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFszXS56KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFszXS53KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs0XS54KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs0XS55KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs0XS56KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs0XS53KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs1XS54KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs1XS55KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs1XS56KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs1XS53KSk7CgkoXzNfY29vcmQgKz0gZmxvYXQyKHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSk7CgkoXzJfY29sb3IgKz0gKE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBfM19jb29yZCkgKiB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs2XS54KSk7CglyZXR1cm4gXzJfY29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gR2F1c3NpYW5Db252b2x1dGlvbl9TdGFnZTFfYzBfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","KYMAAAAABCYIR6AYYAAAAAAAAAAAAAEIQHSACAAACQAAAADQEECQAAAAABIACAAAAAEAAAAAIDEAQ":"BAAAAExTS1NwAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkRWRnZQoJdlF1YWRFZGdlX1N0YWdlMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAQEAAAAAAAABAQAFCAAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMV9jMF9jMDsKaW4gZmxvYXQ0IHZRdWFkRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQUFSZWN0RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDQgcHJldlJlY3QgPSBmbG9hdDQoLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwKTsKCWhhbGYgY292ZXJhZ2U7CglAc3dpdGNoICgxKSAKCXsKCQljYXNlIDA6ICAgIGNhc2UgMjogICAgICAgIGNvdmVyYWdlID0gaGFsZihhbGwoZ3JlYXRlclRoYW4oZmxvYXQ0KHNrX0ZyYWdDb29yZC54eSwgdXJlY3RVbmlmb3JtX1N0YWdlMV9jMF9jMC56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwX2MwLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7CgkJYnJlYWs7CgkJZGVmYXVsdDogICAgICAgIGhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1N0YWdlMV9jMF9jMCksIDAuMCwgMS4wKTsKCQloYWxmMiBkaXN0czIgPSAoZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3KSAtIDEuMDsKCQljb3ZlcmFnZSA9IGRpc3RzMi54ICogZGlzdHMyLnk7Cgl9CglAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gX2lucHV0ICogY292ZXJhZ2U7Cn0KaGFsZjQgQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTFfYzAuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTFfYzAueCAtIGxlbmd0aChkeHkpKSk7CglyZXR1cm4gQUFSZWN0RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQpICogYWxwaGE7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgloYWxmIGVkZ2VBbHBoYTsKCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TdGFnZTAueiA+IDAuMCAmJiB2UXVhZEVkZ2VfU3RhZ2UwLncgPiAwLjApIAoJewoJCWVkZ2VBbHBoYSA9IGhhbGYobWluKG1pbih2UXVhZEVkZ2VfU3RhZ2UwLnosIHZRdWFkRWRnZV9TdGFnZTAudykgKyAwLjUsIDEuMCkpOwoJfQoJZWxzZSAKCXsKCQloYWxmMiBnRiA9IGhhbGYyKGhhbGYoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSksICAgICAgICAgICAgICAgICBoYWxmKDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpblF1YWRFZGdlAAABAAAAAAAAAA==","B2IASAAAAQAAAAABCYIR7777AAOAAAAAAAAAAAAAUABAAAAACAAAAAEASAIQAAAA":"BAAAAExTS1NwAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCWNvbG9yID0gY29sb3IgKiBpbkNvdmVyYWdlOwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAA6AQAAaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","K5JAAAAAAAMAAAAAARMPZ72HPQCFR7H7777QGAAAAACAAAAAACCAYAGEIDZAAAAAAIAAAAAAVAAAAAAAAQAAAABAMQCAAAAA":"BAAAAExTS1NLAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAQEAAAAAAAABAQDoAwAAdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTFfYzAuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxX2MwLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfU3RhZ2UwKSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","K5IAAAAAAAMAAAAAARMPZ72HPQCFR7H7777QGAAAAAAAAAAAWRDQB4AA6AAPAAHQDQAAAAAAEIFQAAAADQGQAAAAQCHAIAAAADMBAAAAAAABKAAAACAAAAAAACCIYAAA":"BAAAAExTS1O1AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzBfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAAADzBgAAdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXN0YXJ0X1N0YWdlMV9jMF9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1ZW5kX1N0YWdlMV9jMF9jMF9jMTsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBMaW5lYXJHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIGhhbGY0KGhhbGYodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzBfYzEsIGhhbGYoX2Nvb3Jkcy54KSk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCW91dENvbG9yID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsR3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMC4wKSkpOwoJfQoJQGlmICh0cnVlKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBvdXRDb2xvcjsKfQpoYWxmNCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3JfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwX2MwKGZhbHNlID8gaGFsZjQoMCkgOiBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCkpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","IGIAAAAAAIAAAAABCYBR6AAAAAAAAAAAACQAEAAAAAIAAAAAQCIBCAAAAA":"BAAAAExTS1MxAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkhhaXJRdWFkRWRnZTsKb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkCgl2SGFpclF1YWRFZGdlX1N0YWdlMCA9IGluSGFpclF1YWRFZGdlOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAA7AwAAdW5pZm9ybSBoYWxmNCB1Q29sb3JfU3RhZ2UwOwp1bmlmb3JtIGhhbGYgdUNvdmVyYWdlX1N0YWdlMDsKaW4gaGFsZjQgdkhhaXJRdWFkRWRnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJaGFsZiBlZGdlQWxwaGE7CgloYWxmMiBkdXZkeCA9IGhhbGYyKGRGZHgodkhhaXJRdWFkRWRnZV9TdGFnZTAueHkpKTsKCWhhbGYyIGR1dmR5ID0gaGFsZjIoZEZkeSh2SGFpclF1YWRFZGdlX1N0YWdlMC54eSkpOwoJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCWVkZ2VBbHBoYSA9IGhhbGYodkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggLSB2SGFpclF1YWRFZGdlX1N0YWdlMC55KTsKCWVkZ2VBbHBoYSA9IHNxcnQoZWRnZUFscGhhICogZWRnZUFscGhhIC8gZG90KGdGLCBnRikpOwoJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KHVDb3ZlcmFnZV9TdGFnZTAgKiBlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAKAAAAaW5Qb3NpdGlvbgAADgAAAGluSGFpclF1YWRFZGdlAAABAAAAAAAAAA==","GFQQAAAAMAAGGADDACRQAAAAMAACHQDCABRQAI7CAMAAAABAAYJAMAAACAAAAAAAIACQAAAAEAAAAAAAEERQAAAAAA":"BAAAAExTS1PfCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQgYWFfYmxvYXRfbXVsdGlwbGllciA9IDE7CglmbG9hdDIgY29ybmVyID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy54eTsKCWZsb2F0MiByYWRpdXNfb3V0c2V0ID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy56dzsKCWZsb2F0MiBhYV9ibG9hdF9kaXJlY3Rpb24gPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UueHk7CglmbG9hdCBpc19saW5lYXJfY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UudzsKCWZsb2F0MiBwaXhlbGxlbmd0aCA9IGludmVyc2VzcXJ0KGZsb2F0Mihkb3Qoc2tldy54eiwgc2tldy54eiksIGRvdChza2V3Lnl3LCBza2V3Lnl3KSkpOwoJZmxvYXQ0IG5vcm1hbGl6ZWRfYXhpc19kaXJzID0gc2tldyAqIHBpeGVsbGVuZ3RoLnh5eHk7CglmbG9hdDIgYXhpc3dpZHRocyA9IChhYnMobm9ybWFsaXplZF9heGlzX2RpcnMueHkpICsgYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnp3KSk7CglmbG9hdDIgYWFfYmxvYXRyYWRpdXMgPSBheGlzd2lkdGhzICogcGl4ZWxsZW5ndGggKiAuNTsKCWZsb2F0NCByYWRpaV9hbmRfbmVpZ2hib3JzID0gcmFkaWlfc2VsZWN0b3IqIGZsb2F0NHg0KHJhZGlpX3gsIHJhZGlpX3ksIHJhZGlpX3gueXh3eiwgcmFkaWlfeS53enl4KTsKCWZsb2F0MiByYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMueHk7CglmbG9hdDIgbmVpZ2hib3JfcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnp3OwoJZmxvYXQgY292ZXJhZ2VfbXVsdGlwbGllciA9IDE7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2VfbXVsdGlwbGllciA9IDEgLyAobWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpKTsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCX0KCWZsb2F0IGNvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLno7CglpZiAoYW55KGxlc3NUaGFuKHJhZGlpLCBhYV9ibG9hdHJhZGl1cyAqIDEuNSkpKSAKCXsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSBzaWduKGNvcm5lcik7CgkJaWYgKGNvdmVyYWdlID4gLjUpIAoJCXsKCQkJYWFfYmxvYXRfZGlyZWN0aW9uID0gLWFhX2Jsb2F0X2RpcmVjdGlvbjsKCQl9CgkJaXNfbGluZWFyX2NvdmVyYWdlID0gMTsKCX0KCWVsc2UgCgl7CgkJcmFkaWkgPSBjbGFtcChyYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJbmVpZ2hib3JfcmFkaWkgPSBjbGFtcChuZWlnaGJvcl9yYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJZmxvYXQyIHNwYWNpbmcgPSAyIC0gcmFkaWkgLSBuZWlnaGJvcl9yYWRpaTsKCQlmbG9hdDIgZXh0cmFfcGFkID0gbWF4KHBpeGVsbGVuZ3RoICogLjA2MjUgLSBzcGFjaW5nLCBmbG9hdDIoMCkpOwoJCXJhZGlpIC09IGV4dHJhX3BhZCAqIC41OwoJfQoJZmxvYXQyIGFhX291dHNldCA9IGFhX2Jsb2F0X2RpcmVjdGlvbiAqIGFhX2Jsb2F0cmFkaXVzICogYWFfYmxvYXRfbXVsdGlwbGllcjsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglpZiAoY292ZXJhZ2UgPiAuNSkgCgl7CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi54ICE9IDAgJiYgdmVydGV4cG9zLnggKiBjb3JuZXIueCA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueCk7CgkJCXZlcnRleHBvcy54ID0gMDsKCQkJdmVydGV4cG9zLnkgKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLnkpICogcGl4ZWxsZW5ndGgueS9waXhlbGxlbmd0aC54OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueCkgLyAoYWJzKGNvcm5lci54KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueSAhPSAwICYmIHZlcnRleHBvcy55ICogY29ybmVyLnkgPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLnkpOwoJCQl2ZXJ0ZXhwb3MueSA9IDA7CgkJCXZlcnRleHBvcy54ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci54KSAqIHBpeGVsbGVuZ3RoLngvcGl4ZWxsZW5ndGgueTsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLnkpIC8gKGFicyhjb3JuZXIueSkgKyBiYWNrc2V0KSArIC41OwoJCX0KCX0KCWZsb2F0MngyIHNrZXdtYXRyaXggPSBmbG9hdDJ4Mihza2V3Lnh5LCBza2V3Lnp3KTsKCWZsb2F0MiBkZXZjb29yZCA9IHZlcnRleHBvcyAqIHNrZXdtYXRyaXggKyB0cmFuc2xhdGU7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TdGFnZTAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAABAQAAAAAAAAEBALUEAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdCBkeDAgPSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5MIC0gc2tfRnJhZ0Nvb3JkLng7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxX2MwLlJCOwoJZmxvYXQyIGR4eSA9IG1heChmbG9hdDIobWF4KGR4MCwgZHh5MS54KSwgZHh5MS55KSwgMC4wKTsKCWhhbGYgdG9wQWxwaGEgPSBoYWxmKHNhdHVyYXRlKHNrX0ZyYWdDb29yZC55IC0gdWlubmVyUmVjdF9TdGFnZTFfYzAuVCkpOwoJaGFsZiBhbHBoYSA9IHRvcEFscGhhICogaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBHckZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CglmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJaGFsZiBjb3ZlcmFnZTsKCWlmICgwID09IHhfcGx1c18xKSAKCXsKCQljb3ZlcmFnZSA9IGhhbGYoeSk7Cgl9CgllbHNlIAoJewoJCWZsb2F0IGZuID0geF9wbHVzXzEgKiAoeF9wbHVzXzEgLSAyKTsKCQlmbiA9IGZtYSh5LHksIGZuKTsKCQlmbG9hdCBmbndpZHRoID0gZndpZHRoKGZuKTsKCQljb3ZlcmFnZSA9IC41IC0gaGFsZihmbi9mbndpZHRoKTsKCQljb3ZlcmFnZSA9IGNsYW1wKGNvdmVyYWdlLCAwLCAxKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGNvdmVyYWdlKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQCAICAAAAA5BIEBAAAAAAAAJQDBAMAACAIAAAAAAKAAQAAAABAAAAACAJACIAAAAA":"BAAAAExTS1OjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMF9jMCkgKiAodW1hdHJpeF9TdGFnZTFfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAXQQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQgPSBjbGFtcChzdWJzZXRDb29yZCwgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMC54eSwgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQCAICAAAAA5BIEBAAAAAAAAJQDBAMAACAIAAAAAAKAAQAAAABAAAAACAJAEIAAAAA":"BAAAAExTS1OjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMF9jMCkgKiAodW1hdHJpeF9TdGFnZTFfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAXQQAAHVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIGluQ29vcmQgPSB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQgPSBjbGFtcChzdWJzZXRDb29yZCwgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMC54eSwgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMC56dyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","K5IACAAAAAMAAAAAARMAAVCEPQCFR7H7777QGAAAAAAAAAAAWRDQB4AA6AAPAAHQDQAAAAAAEIFQAAAADQGQAAAAQCHAIAAAADMBAAAAAAABKAAAABAACAAAACCIYAAA":"BAAAAExTS1MiAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0IHZjb3ZlcmFnZV9TdGFnZTA7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBwb3NpdGlvbiA9IHBvc2l0aW9uLnh5OwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJdmNvdmVyYWdlX1N0YWdlMCA9IGNvdmVyYWdlOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwX2MwKSkgKiBsb2NhbENvb3JkLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAAABIBwAAdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXN0YXJ0X1N0YWdlMV9jMF9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1ZW5kX1N0YWdlMV9jMF9jMF9jMTsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwppbiBmbG9hdCB2Y292ZXJhZ2VfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBMaW5lYXJHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIGhhbGY0KGhhbGYodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNiwgMS4wLCAwLjAsIDAuMCk7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwX2MwKF9pbnB1dCk7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzBfYzEoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBtaXgodXN0YXJ0X1N0YWdlMV9jMF9jMF9jMSwgdWVuZF9TdGFnZTFfYzBfYzBfYzEsIGhhbGYoX2Nvb3Jkcy54KSk7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJaGFsZjQgb3V0Q29sb3I7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlvdXRDb2xvciA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCW91dENvbG9yID0gdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7Cgl9CgllbHNlIGlmICh0LnggPiAxLjApIAoJewoJCW91dENvbG9yID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwX2MwOwoJfQoJZWxzZSAKCXsKCQlvdXRDb2xvciA9IFNpbmdsZUludGVydmFsR3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MwX2MxKF9pbnB1dCwgZmxvYXQyKGhhbGYyKHQueCwgMC4wKSkpOwoJfQoJQGlmICh0cnVlKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBvdXRDb2xvcjsKfQpoYWxmNCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3JfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwX2MwKGZhbHNlID8gaGFsZjQoMCkgOiBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCkpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CglmbG9hdCBjb3ZlcmFnZSA9IHZjb3ZlcmFnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3Nvcl9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSAoaGFsZjQoMS4wKSAtIG91dHB1dF9TdGFnZTEpICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","AWAQGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAAAUABAAAAACAAAAAEBSAI":"BAAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAALcCAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgllZGdlQWxwaGEgKj0gaW5uZXJBbHBoYTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAFIRQWAAAAAAACAQAAAAHIBRAIAAAAAAAKMAYIDAAAACAAAAAACQAEAAAAAIAAAAAQCIBCAAAAA":"BAAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAdREAAHVuaWZvcm0gaGFsZjIgdV8wX0luY3JlbWVudF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdV8xX0tlcm5lbF9TdGFnZTFfYzBbNl07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWZsb2F0MiBpbkNvb3JkID0gX2Nvb3JkczsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZC54ID0gc3Vic2V0Q29vcmQueDsKCWNsYW1wZWRDb29yZC55ID0gY2xhbXAoc3Vic2V0Q29vcmQueSwgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMC55LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwX2MwLncpOwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgKCh1bWF0cml4X1N0YWdlMV9jMF9jMCkgKiBfY29vcmRzLnh5MSkueHkpOwp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF8yX2NvbG9yID0gaGFsZjQoMC4wKTsKCWZsb2F0MiBfM19jb29yZCA9ICh2TG9jYWxDb29yZF9TdGFnZTAgLSBmbG9hdDIoKDExLjAgKiB1XzBfSW5jcmVtZW50X1N0YWdlMV9jMCkpKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzBdLngpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzBdLnkpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzBdLnopKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzBdLncpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzFdLngpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzFdLnkpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzFdLnopKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzFdLncpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzJdLngpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzJdLnkpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzJdLnopKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzJdLncpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzNdLngpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzNdLnkpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzNdLnopKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzNdLncpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzRdLngpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzRdLnkpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzRdLnopKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzRdLncpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzVdLngpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzVdLnkpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzVdLnopKTsKCXJldHVybiBfMl9jb2xvcjsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBHYXVzc2lhbkNvbnZvbHV0aW9uX1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","KYMAAAAABCYIR6AYYAAAAAAAAAAAAAGIQUKAAAAAABAAKAAAAAQAAAAAAAQSGAAA":"BAAAAExTS1NwAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkRWRnZQoJdlF1YWRFZGdlX1N0YWdlMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAQEAAAAAAAABAQBdBgAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZRdWFkRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgQUFSZWN0RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDQgcHJldlJlY3QgPSBmbG9hdDQoLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwKTsKCWhhbGYgY292ZXJhZ2U7CglAc3dpdGNoICgxKSAKCXsKCQljYXNlIDA6ICAgIGNhc2UgMjogICAgICAgIGNvdmVyYWdlID0gaGFsZihhbGwoZ3JlYXRlclRoYW4oZmxvYXQ0KHNrX0ZyYWdDb29yZC54eSwgdXJlY3RVbmlmb3JtX1N0YWdlMV9jMC56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7CgkJYnJlYWs7CgkJZGVmYXVsdDogICAgICAgIGhhbGY0IGRpc3RzNCA9IGNsYW1wKGhhbGY0KDEuMCwgMS4wLCAtMS4wLCAtMS4wKSAqIGhhbGY0KHNrX0ZyYWdDb29yZC54eXh5IC0gdXJlY3RVbmlmb3JtX1N0YWdlMV9jMCksIDAuMCwgMS4wKTsKCQloYWxmMiBkaXN0czIgPSAoZGlzdHM0Lnh5ICsgZGlzdHM0Lnp3KSAtIDEuMDsKCQljb3ZlcmFnZSA9IGRpc3RzMi54ICogZGlzdHMyLnk7Cgl9CglAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJewoJCWNvdmVyYWdlID0gMS4wIC0gY292ZXJhZ2U7Cgl9CglyZXR1cm4gX2lucHV0ICogY292ZXJhZ2U7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgloYWxmIGVkZ2VBbHBoYTsKCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TdGFnZTAueiA+IDAuMCAmJiB2UXVhZEVkZ2VfU3RhZ2UwLncgPiAwLjApIAoJewoJCWVkZ2VBbHBoYSA9IGhhbGYobWluKG1pbih2UXVhZEVkZ2VfU3RhZ2UwLnosIHZRdWFkRWRnZV9TdGFnZTAudykgKyAwLjUsIDEuMCkpOwoJfQoJZWxzZSAKCXsKCQloYWxmMiBnRiA9IGhhbGYyKGhhbGYoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSksICAgICAgICAgICAgICAgICBoYWxmKDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQUFSZWN0RWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb3ZlcmFnZV9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","KYNQAAAABCYIR6AYYAAAAAAAAAAAAADIR4AOAAPAAHQADYBRAAAAAACECQAAAABYDIAAAAAADUEQAAAAWAQQAAAAAAVAAAAAAAAQAAAABAMQC":"BAAAAExTS1OgAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TdGFnZTAgPSBpblF1YWRFZGdlOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAGAkAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzBfYzE7CmluIGZsb2F0NCB2UXVhZEVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gaGFsZjQoaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2LCAxLjAsIDAuMCwgMC4wKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBMaW5lYXJHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIG1peCh1c3RhcnRfU3RhZ2UxX2MwX2MwX2MxLCB1ZW5kX1N0YWdlMV9jMF9jMF9jMSwgaGFsZihfY29vcmRzLngpKTsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMF9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJb3V0Q29sb3IgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglAaWYgKGZhbHNlKSAKCXsKCQlvdXRDb2xvci54eXogKj0gb3V0Q29sb3IudzsKCX0KCXJldHVybiBvdXRDb2xvcjsKfQpoYWxmNCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3JfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwX2MwKGZhbHNlID8gaGFsZjQoMCkgOiBoYWxmNCgxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCkpOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkRWRnZQoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJaGFsZiBlZGdlQWxwaGE7CgloYWxmMiBkdXZkeCA9IGhhbGYyKGRGZHgodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCWlmICh2UXVhZEVkZ2VfU3RhZ2UwLnogPiAwLjAgJiYgdlF1YWRFZGdlX1N0YWdlMC53ID4gMC4wKSAKCXsKCQllZGdlQWxwaGEgPSBoYWxmKG1pbihtaW4odlF1YWRFZGdlX1N0YWdlMC56LCB2UXVhZEVkZ2VfU3RhZ2UwLncpICsgMC41LCAxLjApKTsKCX0KCWVsc2UgCgl7CgkJaGFsZjIgZ0YgPSBoYWxmMihoYWxmKDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHgueCAtIGR1dmR4LnkpLCAgICAgICAgICAgICAgICAgaGFsZigyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR5LnggLSBkdXZkeS55KSk7CgkJZWRnZUFscGhhID0gaGFsZih2UXVhZEVkZ2VfU3RhZ2UwLngqdlF1YWRFZGdlX1N0YWdlMC54IC0gdlF1YWRFZGdlX1N0YWdlMC55KTsKCQllZGdlQWxwaGEgPSBzYXR1cmF0ZSgwLjUgLSBlZGdlQWxwaGEgLyBsZW5ndGgoZ0YpKTsKCX0KCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3Nvcl9TdGFnZTFfYzAob3V0cHV0Q29sb3JfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpblF1YWRFZGdlAAABAAAAAAAAAA==","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAFIRQWAAAAAAQAAQAAAAHIJQAIAAAAAAAKMAYIDAAAQAAAAAAACQAEAAAAAIAAAAAQCIBCAAAAA":"BAAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAdREAAHVuaWZvcm0gaGFsZjIgdV8wX0luY3JlbWVudF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdV8xX0tlcm5lbF9TdGFnZTFfYzBbNl07CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWZsb2F0MiBpbkNvb3JkID0gX2Nvb3JkczsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZC54ID0gY2xhbXAoc3Vic2V0Q29vcmQueCwgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMC54LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgKCh1bWF0cml4X1N0YWdlMV9jMF9jMCkgKiBfY29vcmRzLnh5MSkueHkpOwp9CmhhbGY0IEdhdXNzaWFuQ29udm9sdXRpb25fU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF8yX2NvbG9yID0gaGFsZjQoMC4wKTsKCWZsb2F0MiBfM19jb29yZCA9ICh2TG9jYWxDb29yZF9TdGFnZTAgLSBmbG9hdDIoKDExLjAgKiB1XzBfSW5jcmVtZW50X1N0YWdlMV9jMCkpKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzBdLngpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzBdLnkpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzBdLnopKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzBdLncpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzFdLngpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzFdLnkpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzFdLnopKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzFdLncpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzJdLngpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzJdLnkpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzJdLnopKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzJdLncpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzNdLngpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzNdLnkpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzNdLnopKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzNdLncpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzRdLngpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzRdLnkpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzRdLnopKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzRdLncpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzVdLngpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzVdLnkpKTsKCShfM19jb29yZCArPSBmbG9hdDIodV8wX0luY3JlbWVudF9TdGFnZTFfYzApKTsKCShfMl9jb2xvciArPSAoTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIF8zX2Nvb3JkKSAqIHVfMV9LZXJuZWxfU3RhZ2UxX2MwWzVdLnopKTsKCXJldHVybiBfMl9jb2xvcjsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBHYXVzc2lhbkNvbnZvbHV0aW9uX1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","B2AAQAAAAQAAAAABC3777777AAOAAAAAAAAAAAAAUABAAAAACAAAAAEASAIQAAAA":"BAAAAExTS1M3AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmIGluQ292ZXJhZ2U7Cm91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJdmluQ292ZXJhZ2VfU3RhZ2UwID0gaW5Db3ZlcmFnZTsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAiQEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKaW4gaGFsZiB2aW5Db3ZlcmFnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgloYWxmIGFscGhhID0gMS4wOwoJYWxwaGEgPSB2aW5Db3ZlcmFnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChhbHBoYSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","K4JAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAABAAAAAABBAMACAAUAAAABAAAAAAABBEMAAA":"BAAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAA2wEAAHVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CglmbG9hdDIgdGV4Q29vcmQ7Cgl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9ICgoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdGV4Q29vcmQpICogaGFsZjQoMSkpKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAATIRQKAAFQAAQCBAAAAAHIJRAIAAAAAAAKMAYIDAAAQCAAAAAAAAKAAQAAAABAAAAACAJAEIAAA":"BAAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAAAAtBcAAHVuaWZvcm0gaGFsZjQgdUtlcm5lbF9TdGFnZTFfYzBbN107CnVuaWZvcm0gaGFsZjIgdUtlcm5lbE9mZnNldF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZiB1R2Fpbl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZiB1Qmlhc19TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQ0IHVjbGFtcF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWZsb2F0MiBpbkNvb3JkID0gX2Nvb3JkczsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZCA9IGNsYW1wKHN1YnNldENvb3JkLCB1Y2xhbXBfU3RhZ2UxX2MwX2MwX2MwLnh5LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwX2MwLnp3KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7CglyZXR1cm4gdGV4dHVyZUNvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzBfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKfQpoYWxmNCBNYXRyaXhDb252b2x1dGlvbl9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgc3VtID0gaGFsZjQoMCk7CglmbG9hdDIgY29vcmQgPSB2TG9jYWxDb29yZF9TdGFnZTAgLSB1S2VybmVsT2Zmc2V0X1N0YWdlMV9jMDsKCXsKCQloYWxmIGs7CgkJaGFsZjIgc291cmNlT2Zmc2V0OwoJCXNvdXJjZU9mZnNldCA9IGhhbGYyKDAsIDApOwoJCWsgPSB1S2VybmVsX1N0YWdlMV9jMFswXVswXTsKCQloYWxmNCBjID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIGNvb3JkICsgc291cmNlT2Zmc2V0KTsKCQlzdW0gKz0gYyAqIGs7Cgl9Cgl7CgkJaGFsZiBrOwoJCWhhbGYyIHNvdXJjZU9mZnNldDsKCQlzb3VyY2VPZmZzZXQgPSBoYWxmMigwLCAxKTsKCQlrID0gdUtlcm5lbF9TdGFnZTFfYzBbMV1bMV07CgkJaGFsZjQgYyA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0LCBjb29yZCArIHNvdXJjZU9mZnNldCk7CgkJc3VtICs9IGMgKiBrOwoJfQoJewoJCWhhbGYgazsKCQloYWxmMiBzb3VyY2VPZmZzZXQ7CgkJc291cmNlT2Zmc2V0ID0gaGFsZjIoMCwgMik7CgkJayA9IHVLZXJuZWxfU3RhZ2UxX2MwWzJdWzJdOwoJCWhhbGY0IGMgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgY29vcmQgKyBzb3VyY2VPZmZzZXQpOwoJCXN1bSArPSBjICogazsKCX0KCXsKCQloYWxmIGs7CgkJaGFsZjIgc291cmNlT2Zmc2V0OwoJCXNvdXJjZU9mZnNldCA9IGhhbGYyKDAsIDMpOwoJCWsgPSB1S2VybmVsX1N0YWdlMV9jMFszXVszXTsKCQloYWxmNCBjID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIGNvb3JkICsgc291cmNlT2Zmc2V0KTsKCQlzdW0gKz0gYyAqIGs7Cgl9Cgl7CgkJaGFsZiBrOwoJCWhhbGYyIHNvdXJjZU9mZnNldDsKCQlzb3VyY2VPZmZzZXQgPSBoYWxmMigwLCA0KTsKCQlrID0gdUtlcm5lbF9TdGFnZTFfYzBbNV1bMF07CgkJaGFsZjQgYyA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0LCBjb29yZCArIHNvdXJjZU9mZnNldCk7CgkJc3VtICs9IGMgKiBrOwoJfQoJewoJCWhhbGYgazsKCQloYWxmMiBzb3VyY2VPZmZzZXQ7CgkJc291cmNlT2Zmc2V0ID0gaGFsZjIoMSwgMCk7CgkJayA9IHVLZXJuZWxfU3RhZ2UxX2MwWzBdWzFdOwoJCWhhbGY0IGMgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgY29vcmQgKyBzb3VyY2VPZmZzZXQpOwoJCXN1bSArPSBjICogazsKCX0KCXsKCQloYWxmIGs7CgkJaGFsZjIgc291cmNlT2Zmc2V0OwoJCXNvdXJjZU9mZnNldCA9IGhhbGYyKDEsIDEpOwoJCWsgPSB1S2VybmVsX1N0YWdlMV9jMFsxXVsyXTsKCQloYWxmNCBjID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIGNvb3JkICsgc291cmNlT2Zmc2V0KTsKCQlzdW0gKz0gYyAqIGs7Cgl9Cgl7CgkJaGFsZiBrOwoJCWhhbGYyIHNvdXJjZU9mZnNldDsKCQlzb3VyY2VPZmZzZXQgPSBoYWxmMigxLCAyKTsKCQlrID0gdUtlcm5lbF9TdGFnZTFfYzBbMl1bM107CgkJaGFsZjQgYyA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0LCBjb29yZCArIHNvdXJjZU9mZnNldCk7CgkJc3VtICs9IGMgKiBrOwoJfQoJewoJCWhhbGYgazsKCQloYWxmMiBzb3VyY2VPZmZzZXQ7CgkJc291cmNlT2Zmc2V0ID0gaGFsZjIoMSwgMyk7CgkJayA9IHVLZXJuZWxfU3RhZ2UxX2MwWzRdWzBdOwoJCWhhbGY0IGMgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgY29vcmQgKyBzb3VyY2VPZmZzZXQpOwoJCXN1bSArPSBjICogazsKCX0KCXsKCQloYWxmIGs7CgkJaGFsZjIgc291cmNlT2Zmc2V0OwoJCXNvdXJjZU9mZnNldCA9IGhhbGYyKDEsIDQpOwoJCWsgPSB1S2VybmVsX1N0YWdlMV9jMFs1XVsxXTsKCQloYWxmNCBjID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIGNvb3JkICsgc291cmNlT2Zmc2V0KTsKCQlzdW0gKz0gYyAqIGs7Cgl9Cgl7CgkJaGFsZiBrOwoJCWhhbGYyIHNvdXJjZU9mZnNldDsKCQlzb3VyY2VPZmZzZXQgPSBoYWxmMigyLCAwKTsKCQlrID0gdUtlcm5lbF9TdGFnZTFfYzBbMF1bMl07CgkJaGFsZjQgYyA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0LCBjb29yZCArIHNvdXJjZU9mZnNldCk7CgkJc3VtICs9IGMgKiBrOwoJfQoJewoJCWhhbGYgazsKCQloYWxmMiBzb3VyY2VPZmZzZXQ7CgkJc291cmNlT2Zmc2V0ID0gaGFsZjIoMiwgMSk7CgkJayA9IHVLZXJuZWxfU3RhZ2UxX2MwWzFdWzNdOwoJCWhhbGY0IGMgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgY29vcmQgKyBzb3VyY2VPZmZzZXQpOwoJCXN1bSArPSBjICogazsKCX0KCXsKCQloYWxmIGs7CgkJaGFsZjIgc291cmNlT2Zmc2V0OwoJCXNvdXJjZU9mZnNldCA9IGhhbGYyKDIsIDIpOwoJCWsgPSB1S2VybmVsX1N0YWdlMV9jMFszXVswXTsKCQloYWxmNCBjID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIGNvb3JkICsgc291cmNlT2Zmc2V0KTsKCQlzdW0gKz0gYyAqIGs7Cgl9Cgl7CgkJaGFsZiBrOwoJCWhhbGYyIHNvdXJjZU9mZnNldDsKCQlzb3VyY2VPZmZzZXQgPSBoYWxmMigyLCAzKTsKCQlrID0gdUtlcm5lbF9TdGFnZTFfYzBbNF1bMV07CgkJaGFsZjQgYyA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0LCBjb29yZCArIHNvdXJjZU9mZnNldCk7CgkJc3VtICs9IGMgKiBrOwoJfQoJewoJCWhhbGYgazsKCQloYWxmMiBzb3VyY2VPZmZzZXQ7CgkJc291cmNlT2Zmc2V0ID0gaGFsZjIoMiwgNCk7CgkJayA9IHVLZXJuZWxfU3RhZ2UxX2MwWzVdWzJdOwoJCWhhbGY0IGMgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgY29vcmQgKyBzb3VyY2VPZmZzZXQpOwoJCXN1bSArPSBjICogazsKCX0KCXsKCQloYWxmIGs7CgkJaGFsZjIgc291cmNlT2Zmc2V0OwoJCXNvdXJjZU9mZnNldCA9IGhhbGYyKDMsIDApOwoJCWsgPSB1S2VybmVsX1N0YWdlMV9jMFswXVszXTsKCQloYWxmNCBjID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIGNvb3JkICsgc291cmNlT2Zmc2V0KTsKCQlzdW0gKz0gYyAqIGs7Cgl9Cgl7CgkJaGFsZiBrOwoJCWhhbGYyIHNvdXJjZU9mZnNldDsKCQlzb3VyY2VPZmZzZXQgPSBoYWxmMigzLCAxKTsKCQlrID0gdUtlcm5lbF9TdGFnZTFfYzBbMl1bMF07CgkJaGFsZjQgYyA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0LCBjb29yZCArIHNvdXJjZU9mZnNldCk7CgkJc3VtICs9IGMgKiBrOwoJfQoJewoJCWhhbGYgazsKCQloYWxmMiBzb3VyY2VPZmZzZXQ7CgkJc291cmNlT2Zmc2V0ID0gaGFsZjIoMywgMik7CgkJayA9IHVLZXJuZWxfU3RhZ2UxX2MwWzNdWzFdOwoJCWhhbGY0IGMgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgY29vcmQgKyBzb3VyY2VPZmZzZXQpOwoJCXN1bSArPSBjICogazsKCX0KCXsKCQloYWxmIGs7CgkJaGFsZjIgc291cmNlT2Zmc2V0OwoJCXNvdXJjZU9mZnNldCA9IGhhbGYyKDMsIDMpOwoJCWsgPSB1S2VybmVsX1N0YWdlMV9jMFs0XVsyXTsKCQloYWxmNCBjID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIGNvb3JkICsgc291cmNlT2Zmc2V0KTsKCQlzdW0gKz0gYyAqIGs7Cgl9Cgl7CgkJaGFsZiBrOwoJCWhhbGYyIHNvdXJjZU9mZnNldDsKCQlzb3VyY2VPZmZzZXQgPSBoYWxmMigzLCA0KTsKCQlrID0gdUtlcm5lbF9TdGFnZTFfYzBbNV1bM107CgkJaGFsZjQgYyA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0LCBjb29yZCArIHNvdXJjZU9mZnNldCk7CgkJc3VtICs9IGMgKiBrOwoJfQoJewoJCWhhbGYgazsKCQloYWxmMiBzb3VyY2VPZmZzZXQ7CgkJc291cmNlT2Zmc2V0ID0gaGFsZjIoNCwgMCk7CgkJayA9IHVLZXJuZWxfU3RhZ2UxX2MwWzFdWzBdOwoJCWhhbGY0IGMgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgY29vcmQgKyBzb3VyY2VPZmZzZXQpOwoJCXN1bSArPSBjICogazsKCX0KCXsKCQloYWxmIGs7CgkJaGFsZjIgc291cmNlT2Zmc2V0OwoJCXNvdXJjZU9mZnNldCA9IGhhbGYyKDQsIDEpOwoJCWsgPSB1S2VybmVsX1N0YWdlMV9jMFsyXVsxXTsKCQloYWxmNCBjID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIGNvb3JkICsgc291cmNlT2Zmc2V0KTsKCQlzdW0gKz0gYyAqIGs7Cgl9Cgl7CgkJaGFsZiBrOwoJCWhhbGYyIHNvdXJjZU9mZnNldDsKCQlzb3VyY2VPZmZzZXQgPSBoYWxmMig0LCAyKTsKCQlrID0gdUtlcm5lbF9TdGFnZTFfYzBbM11bMl07CgkJaGFsZjQgYyA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0LCBjb29yZCArIHNvdXJjZU9mZnNldCk7CgkJc3VtICs9IGMgKiBrOwoJfQoJewoJCWhhbGYgazsKCQloYWxmMiBzb3VyY2VPZmZzZXQ7CgkJc291cmNlT2Zmc2V0ID0gaGFsZjIoNCwgMyk7CgkJayA9IHVLZXJuZWxfU3RhZ2UxX2MwWzRdWzNdOwoJCWhhbGY0IGMgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCwgY29vcmQgKyBzb3VyY2VPZmZzZXQpOwoJCXN1bSArPSBjICogazsKCX0KCXsKCQloYWxmIGs7CgkJaGFsZjIgc291cmNlT2Zmc2V0OwoJCXNvdXJjZU9mZnNldCA9IGhhbGYyKDQsIDQpOwoJCWsgPSB1S2VybmVsX1N0YWdlMV9jMFs2XVswXTsKCQloYWxmNCBjID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsIGNvb3JkICsgc291cmNlT2Zmc2V0KTsKCQlzdW0gKz0gYyAqIGs7Cgl9CgloYWxmNCBjb2xvcjsKCWNvbG9yID0gc3VtICogdUdhaW5fU3RhZ2UxX2MwICsgdUJpYXNfU3RhZ2UxX2MwOwoJY29sb3IuYSA9IHNhdHVyYXRlKGNvbG9yLmEpOwoJY29sb3IucmdiID0gY2xhbXAoY29sb3IucmdiLCAwLjAsIGNvbG9yLmEpOwoJcmV0dXJuIGNvbG9yOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMCA9IGhhbGY0KDEpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IE1hdHJpeENvbnZvbHV0aW9uX1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","K6QACAAAAAGAAAAAAIWAAKRCH37P6BZQ737QCAAAAAAAAAAAIACQAAAAEAAAAAAAEERQAAA":"BAAAAExTS1O9AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQgY292ZXJhZ2U7CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDQgZ2VvbVN1YnNldDsKZmxhdCBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGZsb2F0IHZjb3ZlcmFnZV9TdGFnZTA7CmZsYXQgb3V0IGZsb2F0NCB2Z2VvbVN1YnNldF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQyIHBvc2l0aW9uID0gcG9zaXRpb24ueHk7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2Y292ZXJhZ2VfU3RhZ2UwID0gY292ZXJhZ2U7Cgl2Z2VvbVN1YnNldF9TdGFnZTAgPSBnZW9tU3Vic2V0OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAABAQAAAAAAAAEBAL0CAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0IHZjb3ZlcmFnZV9TdGFnZTA7CmZsYXQgaW4gZmxvYXQ0IHZnZW9tU3Vic2V0X1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCWZsb2F0IGNvdmVyYWdlID0gdmNvdmVyYWdlX1N0YWdlMDsKCWZsb2F0NCBnZW9TdWJzZXQ7CglnZW9TdWJzZXQgPSB2Z2VvbVN1YnNldF9TdGFnZTA7CgloYWxmNCBkaXN0czQgPSBjbGFtcChoYWxmNCgxLCAxLCAtMSwgLTEpICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSBnZW9TdWJzZXQpLCAwLCAxKTsKCWhhbGYyIGRpc3RzMiA9IGRpc3RzNC54eSArIGRpc3RzNC56dyAtIDE7CgloYWxmIHN1YnNldENvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCWNvdmVyYWdlID0gbWluKGNvdmVyYWdlLCBzdWJzZXRDb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChoYWxmKGNvdmVyYWdlKSk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAEAAAACAAAAHBvc2l0aW9uCAAAAGNvdmVyYWdlBQAAAGNvbG9yAAAACgAAAGdlb21TdWJzZXQAAAEAAAAAAAAA","GFQQAAAAMAAGGADDACRQAAAAMAACHQDCABRQAI7CAMAAAABAA2JAOAAAKAAAAAGAQUKAAAAAABAAKAAAAAQAAAAAAAQSGAAA":"BAAAAExTS1PfCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQgYWFfYmxvYXRfbXVsdGlwbGllciA9IDE7CglmbG9hdDIgY29ybmVyID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy54eTsKCWZsb2F0MiByYWRpdXNfb3V0c2V0ID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy56dzsKCWZsb2F0MiBhYV9ibG9hdF9kaXJlY3Rpb24gPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UueHk7CglmbG9hdCBpc19saW5lYXJfY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UudzsKCWZsb2F0MiBwaXhlbGxlbmd0aCA9IGludmVyc2VzcXJ0KGZsb2F0Mihkb3Qoc2tldy54eiwgc2tldy54eiksIGRvdChza2V3Lnl3LCBza2V3Lnl3KSkpOwoJZmxvYXQ0IG5vcm1hbGl6ZWRfYXhpc19kaXJzID0gc2tldyAqIHBpeGVsbGVuZ3RoLnh5eHk7CglmbG9hdDIgYXhpc3dpZHRocyA9IChhYnMobm9ybWFsaXplZF9heGlzX2RpcnMueHkpICsgYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnp3KSk7CglmbG9hdDIgYWFfYmxvYXRyYWRpdXMgPSBheGlzd2lkdGhzICogcGl4ZWxsZW5ndGggKiAuNTsKCWZsb2F0NCByYWRpaV9hbmRfbmVpZ2hib3JzID0gcmFkaWlfc2VsZWN0b3IqIGZsb2F0NHg0KHJhZGlpX3gsIHJhZGlpX3ksIHJhZGlpX3gueXh3eiwgcmFkaWlfeS53enl4KTsKCWZsb2F0MiByYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMueHk7CglmbG9hdDIgbmVpZ2hib3JfcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnp3OwoJZmxvYXQgY292ZXJhZ2VfbXVsdGlwbGllciA9IDE7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2VfbXVsdGlwbGllciA9IDEgLyAobWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpKTsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCX0KCWZsb2F0IGNvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLno7CglpZiAoYW55KGxlc3NUaGFuKHJhZGlpLCBhYV9ibG9hdHJhZGl1cyAqIDEuNSkpKSAKCXsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSBzaWduKGNvcm5lcik7CgkJaWYgKGNvdmVyYWdlID4gLjUpIAoJCXsKCQkJYWFfYmxvYXRfZGlyZWN0aW9uID0gLWFhX2Jsb2F0X2RpcmVjdGlvbjsKCQl9CgkJaXNfbGluZWFyX2NvdmVyYWdlID0gMTsKCX0KCWVsc2UgCgl7CgkJcmFkaWkgPSBjbGFtcChyYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJbmVpZ2hib3JfcmFkaWkgPSBjbGFtcChuZWlnaGJvcl9yYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJZmxvYXQyIHNwYWNpbmcgPSAyIC0gcmFkaWkgLSBuZWlnaGJvcl9yYWRpaTsKCQlmbG9hdDIgZXh0cmFfcGFkID0gbWF4KHBpeGVsbGVuZ3RoICogLjA2MjUgLSBzcGFjaW5nLCBmbG9hdDIoMCkpOwoJCXJhZGlpIC09IGV4dHJhX3BhZCAqIC41OwoJfQoJZmxvYXQyIGFhX291dHNldCA9IGFhX2Jsb2F0X2RpcmVjdGlvbiAqIGFhX2Jsb2F0cmFkaXVzICogYWFfYmxvYXRfbXVsdGlwbGllcjsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglpZiAoY292ZXJhZ2UgPiAuNSkgCgl7CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi54ICE9IDAgJiYgdmVydGV4cG9zLnggKiBjb3JuZXIueCA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueCk7CgkJCXZlcnRleHBvcy54ID0gMDsKCQkJdmVydGV4cG9zLnkgKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLnkpICogcGl4ZWxsZW5ndGgueS9waXhlbGxlbmd0aC54OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueCkgLyAoYWJzKGNvcm5lci54KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueSAhPSAwICYmIHZlcnRleHBvcy55ICogY29ybmVyLnkgPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLnkpOwoJCQl2ZXJ0ZXhwb3MueSA9IDA7CgkJCXZlcnRleHBvcy54ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci54KSAqIHBpeGVsbGVuZ3RoLngvcGl4ZWxsZW5ndGgueTsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLnkpIC8gKGFicyhjb3JuZXIueSkgKyBiYWNrc2V0KSArIC41OwoJCX0KCX0KCWZsb2F0MngyIHNrZXdtYXRyaXggPSBmbG9hdDJ4Mihza2V3Lnh5LCBza2V3Lnp3KTsKCWZsb2F0MiBkZXZjb29yZCA9IHZlcnRleHBvcyAqIHNrZXdtYXRyaXggKyB0cmFuc2xhdGU7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TdGFnZTAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAABAQAAAAAAAAEBACMHAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJaGFsZiBjb3ZlcmFnZTsKCUBzd2l0Y2ggKDEpIAoJewoJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwX2MwLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTFfYzBfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQlicmVhazsKCQlkZWZhdWx0OiAgICAgICAgaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwX2MwKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCUBpZiAoMSA9PSAyIHx8IDEgPT0gMykgCgl7CgkJY292ZXJhZ2UgPSAxLjAgLSBjb3ZlcmFnZTsKCX0KCXJldHVybiBfaW5wdXQgKiBjb3ZlcmFnZTsKfQpoYWxmNCBDaXJjdWxhclJSZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwX2MwKF9pbnB1dCkgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1N0YWdlMC54LCB5PXZhcmNjb29yZF9TdGFnZTAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","AWQQGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAAAUABAAAAACAAAAAEBSAI":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAC3AgAAaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGYgZGlzdGFuY2VUb0lubmVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKGQgLSBjaXJjbGVFZGdlLncpKTsKCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","BYAAQAAAAQAAAAABC3777777777QAAAAAAAAAAAAUABAAAAACAAAAAEASAIQAAAA":"BAAAAExTS1PkAAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAPwEAAHVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdUNvbG9yX1N0YWdlMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAKAAAAaW5Qb3NpdGlvbgAAAQAAAAAAAAA=","GFQQAAAAMAAGGADDACRQAAAAMAACHQDCABRQAI7CAMAAAABAA2JAOAAACAAAAAAAIACQAAAAEAAAAAAAEERQAAAAAA":"BAAAAExTS1PfCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQgYWFfYmxvYXRfbXVsdGlwbGllciA9IDE7CglmbG9hdDIgY29ybmVyID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy54eTsKCWZsb2F0MiByYWRpdXNfb3V0c2V0ID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy56dzsKCWZsb2F0MiBhYV9ibG9hdF9kaXJlY3Rpb24gPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UueHk7CglmbG9hdCBpc19saW5lYXJfY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UudzsKCWZsb2F0MiBwaXhlbGxlbmd0aCA9IGludmVyc2VzcXJ0KGZsb2F0Mihkb3Qoc2tldy54eiwgc2tldy54eiksIGRvdChza2V3Lnl3LCBza2V3Lnl3KSkpOwoJZmxvYXQ0IG5vcm1hbGl6ZWRfYXhpc19kaXJzID0gc2tldyAqIHBpeGVsbGVuZ3RoLnh5eHk7CglmbG9hdDIgYXhpc3dpZHRocyA9IChhYnMobm9ybWFsaXplZF9heGlzX2RpcnMueHkpICsgYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnp3KSk7CglmbG9hdDIgYWFfYmxvYXRyYWRpdXMgPSBheGlzd2lkdGhzICogcGl4ZWxsZW5ndGggKiAuNTsKCWZsb2F0NCByYWRpaV9hbmRfbmVpZ2hib3JzID0gcmFkaWlfc2VsZWN0b3IqIGZsb2F0NHg0KHJhZGlpX3gsIHJhZGlpX3ksIHJhZGlpX3gueXh3eiwgcmFkaWlfeS53enl4KTsKCWZsb2F0MiByYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMueHk7CglmbG9hdDIgbmVpZ2hib3JfcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnp3OwoJZmxvYXQgY292ZXJhZ2VfbXVsdGlwbGllciA9IDE7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2VfbXVsdGlwbGllciA9IDEgLyAobWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpKTsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCX0KCWZsb2F0IGNvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLno7CglpZiAoYW55KGxlc3NUaGFuKHJhZGlpLCBhYV9ibG9hdHJhZGl1cyAqIDEuNSkpKSAKCXsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSBzaWduKGNvcm5lcik7CgkJaWYgKGNvdmVyYWdlID4gLjUpIAoJCXsKCQkJYWFfYmxvYXRfZGlyZWN0aW9uID0gLWFhX2Jsb2F0X2RpcmVjdGlvbjsKCQl9CgkJaXNfbGluZWFyX2NvdmVyYWdlID0gMTsKCX0KCWVsc2UgCgl7CgkJcmFkaWkgPSBjbGFtcChyYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJbmVpZ2hib3JfcmFkaWkgPSBjbGFtcChuZWlnaGJvcl9yYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJZmxvYXQyIHNwYWNpbmcgPSAyIC0gcmFkaWkgLSBuZWlnaGJvcl9yYWRpaTsKCQlmbG9hdDIgZXh0cmFfcGFkID0gbWF4KHBpeGVsbGVuZ3RoICogLjA2MjUgLSBzcGFjaW5nLCBmbG9hdDIoMCkpOwoJCXJhZGlpIC09IGV4dHJhX3BhZCAqIC41OwoJfQoJZmxvYXQyIGFhX291dHNldCA9IGFhX2Jsb2F0X2RpcmVjdGlvbiAqIGFhX2Jsb2F0cmFkaXVzICogYWFfYmxvYXRfbXVsdGlwbGllcjsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglpZiAoY292ZXJhZ2UgPiAuNSkgCgl7CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi54ICE9IDAgJiYgdmVydGV4cG9zLnggKiBjb3JuZXIueCA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueCk7CgkJCXZlcnRleHBvcy54ID0gMDsKCQkJdmVydGV4cG9zLnkgKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLnkpICogcGl4ZWxsZW5ndGgueS9waXhlbGxlbmd0aC54OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueCkgLyAoYWJzKGNvcm5lci54KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueSAhPSAwICYmIHZlcnRleHBvcy55ICogY29ybmVyLnkgPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLnkpOwoJCQl2ZXJ0ZXhwb3MueSA9IDA7CgkJCXZlcnRleHBvcy54ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci54KSAqIHBpeGVsbGVuZ3RoLngvcGl4ZWxsZW5ndGgueTsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLnkpIC8gKGFicyhjb3JuZXIueSkgKyBiYWNrc2V0KSArIC41OwoJCX0KCX0KCWZsb2F0MngyIHNrZXdtYXRyaXggPSBmbG9hdDJ4Mihza2V3Lnh5LCBza2V3Lnp3KTsKCWZsb2F0MiBkZXZjb29yZCA9IHZlcnRleHBvcyAqIHNrZXdtYXRyaXggKyB0cmFuc2xhdGU7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TdGFnZTAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAABAQAAAAAAAAEBAFMEAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1N0YWdlMC54LCB5PXZhcmNjb29yZF9TdGFnZTAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","K4QAAAAAAAGAAAAAAIWP57ZDH37P7777777QCAAAAAAAAAAAMIQHSAAAAAAQAAAAABKAAAAAAABAAAAACAZAEAAAAA":"BAAAAExTS1P0AAAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmZsYXQgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBABEDAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwpmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTFfYzAuTFQgLSBza19GcmFnQ29vcmQueHk7CglmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxX2MwLlJCOwoJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwLnggLSBsZW5ndGgoZHh5KSkpOwoJcmV0dXJuIF9pbnB1dCAqIGFscGhhOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","K5IAAAAAAAMAAAAAARMPZ72HPQCFR7H7777QGAAAAAAAAAAAWRDQB4AA6AAPAAHQDQAAAAAAEIFQAAAADQGQAAAAQCHAIAAAAAEBCAAAAAABKAAAACAAAAAAACCIYAAA":"BAAAAExTS1O1AQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gaGFsZjQgY29sb3I7CmluIGZsb2F0MiBsb2NhbENvb3JkOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzBfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAAADgBgAAdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXN0YXJ0X1N0YWdlMV9jMF9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1ZW5kX1N0YWdlMV9jMF9jMF9jMTsKZmxhdCBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIGhhbGY0KGhhbGYobGVuZ3RoKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkpLCAxLjAsIDAuMCwgMC4wKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBSYWRpYWxHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIG1peCh1c3RhcnRfU3RhZ2UxX2MwX2MwX2MxLCB1ZW5kX1N0YWdlMV9jMF9jMF9jMSwgaGFsZihfY29vcmRzLngpKTsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMF9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJb3V0Q29sb3IgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIG91dENvbG9yOwp9CmhhbGY0IE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3Nvcl9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzBfYzAoZmFsc2UgPyBoYWxmNCgwKSA6IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKSk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3JfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","KYNQAAAABCYIR6AYYAAAAAAAAAAAAADIR4AOAAPAAHQADYBZAAAAAACECYAAAABYDIAAAAAADUEQAAAAWAQQAAAAAAVAAAAAAAAQAAAABAMQC":"BAAAAExTS1OgAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5RdWFkRWRnZTsKb3V0IGZsb2F0NCB2UXVhZEVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZEVkZ2UKCXZRdWFkRWRnZV9TdGFnZTAgPSBpblF1YWRFZGdlOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSB1bG9jYWxNYXRyaXhfU3RhZ2UwLnh6ICogaW5Qb3NpdGlvbiArIHVsb2NhbE1hdHJpeF9TdGFnZTAueXc7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzBfYzApKSAqIF90bXBfMV9pblBvc2l0aW9uLnh5MSkueHk7Cgl9Cn0KAAAAAAAAAAAAAAAAFwkAAHVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzBfYzE7CmluIGZsb2F0NCB2UXVhZEVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gaGFsZjQoaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2LCAxLjAsIDAuMCwgMC4wKTsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCXJldHVybiBMaW5lYXJHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzBfYzAoX2lucHV0KTsKfQpoYWxmNCBTaW5nbGVJbnRlcnZhbEdyYWRpZW50Q29sb3JpemVyX1N0YWdlMV9jMF9jMF9jMShoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJcmV0dXJuIG1peCh1c3RhcnRfU3RhZ2UxX2MwX2MwX2MxLCB1ZW5kX1N0YWdlMV9jMF9jMF9jMSwgaGFsZihfY29vcmRzLngpKTsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IHQgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCk7CgloYWxmNCBvdXRDb2xvcjsKCWlmICghdHJ1ZSAmJiB0LnkgPCAwLjApIAoJewoJCW91dENvbG9yID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJb3V0Q29sb3IgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMF9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJb3V0Q29sb3IgPSB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzBfYzA7Cgl9CgllbHNlIAoJewoJCW91dENvbG9yID0gU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzBfYzEoX2lucHV0LCBmbG9hdDIoaGFsZjIodC54LCAwLjApKSk7Cgl9CglAaWYgKHRydWUpIAoJewoJCW91dENvbG9yLnh5eiAqPSBvdXRDb2xvci53OwoJfQoJcmV0dXJuIG91dENvbG9yOwp9CmhhbGY0IE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3Nvcl9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIENsYW1wZWRHcmFkaWVudEVmZmVjdF9TdGFnZTFfYzBfYzAoZmFsc2UgPyBoYWxmNCgwKSA6IGhhbGY0KDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDAsIDEuMDAwMDAwKSk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgloYWxmIGVkZ2VBbHBoYTsKCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgloYWxmMiBkdXZkeSA9IGhhbGYyKGRGZHkodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJaWYgKHZRdWFkRWRnZV9TdGFnZTAueiA+IDAuMCAmJiB2UXVhZEVkZ2VfU3RhZ2UwLncgPiAwLjApIAoJewoJCWVkZ2VBbHBoYSA9IGhhbGYobWluKG1pbih2UXVhZEVkZ2VfU3RhZ2UwLnosIHZRdWFkRWRnZV9TdGFnZTAudykgKyAwLjUsIDEuMCkpOwoJfQoJZWxzZSAKCXsKCQloYWxmMiBnRiA9IGhhbGYyKGhhbGYoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSksICAgICAgICAgICAgICAgICBoYWxmKDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHkueCAtIGR1dmR5LnkpKTsKCQllZGdlQWxwaGEgPSBoYWxmKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCWVkZ2VBbHBoYSA9IHNhdHVyYXRlKDAuNSAtIGVkZ2VBbHBoYSAvIGxlbmd0aChnRikpOwoJfQoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gT3ZlcnJpZGVJbnB1dEZyYWdtZW50UHJvY2Vzc29yX1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAoAAABpblF1YWRFZGdlAAABAAAAAAAAAA==","DIQAAAAAMAAAAABAYAROFA2CAIAAAABAAAAAAAAAAAACAF2SAAAAAAAACUAAAAEAAAAAAAEERQAAA":"BAAAAExTS1NQAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBmbG9hdCB2VGV4SW5kZXhfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNTaXplSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSBmbG9hdCh0ZXhJZHgpOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChpblBvc2l0aW9uLnggLCBpblBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAO0EAAB1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1N0YWdlMDsKaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBBQVJlY3RFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJaGFsZiBjb3ZlcmFnZTsKCUBzd2l0Y2ggKDEpIAoJewoJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgY292ZXJhZ2UgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQlicmVhazsKCQlkZWZhdWx0OiAgICAgICAgaGFsZjQgZGlzdHM0ID0gY2xhbXAoaGFsZjQoMS4wLCAxLjAsIC0xLjAsIC0xLjApICogaGFsZjQoc2tfRnJhZ0Nvb3JkLnh5eHkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxX2MwKSwgMC4wLCAxLjApOwoJCWhhbGYyIGRpc3RzMiA9IChkaXN0czQueHkgKyBkaXN0czQuencpIC0gMS4wOwoJCWNvdmVyYWdlID0gZGlzdHMyLnggKiBkaXN0czIueTsKCX0KCUBpZiAoMSA9PSAyIHx8IDEgPT0gMykgCgl7CgkJY292ZXJhZ2UgPSAxLjAgLSBjb3ZlcmFnZTsKCX0KCXJldHVybiBfaW5wdXQgKiBjb3ZlcmFnZTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB2VGV4dHVyZUNvb3Jkc19TdGFnZTApLnJycnI7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQUFSZWN0RWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb3ZlcmFnZV9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","BYIBQAAAAQAAAAABCYIR7777777QAAAAAAAAAAAAUABAAAAACAAAAAEASAIQAAAA":"BAAAAExTS1NnAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwpvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBjb2xvciA9IGluQ29sb3I7Cgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAOgEAAGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgABAAAAAAAAAA==","AWQAGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAIAXCIAEAAAAAAKQAAAAAAAIAAAAAQGIBAA":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQAaBQAAdW5pZm9ybSBmbG9hdDQgdXJlY3RVbmlmb3JtX1N0YWdlMV9jMDsKaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwppbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgloYWxmIGNvdmVyYWdlOwoJQHN3aXRjaCAoMSkgCgl7CgkJY2FzZSAwOiAgICBjYXNlIDI6ICAgICAgICBjb3ZlcmFnZSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzAuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1N0YWdlMV9jMC54eSwgc2tfRnJhZ0Nvb3JkLnh5KSkpID8gMSA6IDApOwoJCWJyZWFrOwoJCWRlZmF1bHQ6ICAgICAgICBoYWxmNCBkaXN0czQgPSBjbGFtcChoYWxmNCgxLjAsIDEuMCwgLTEuMCwgLTEuMCkgKiBoYWxmNChza19GcmFnQ29vcmQueHl4eSAtIHVyZWN0VW5pZm9ybV9TdGFnZTFfYzApLCAwLjAsIDEuMCk7CgkJaGFsZjIgZGlzdHMyID0gKGRpc3RzNC54eSArIGRpc3RzNC56dykgLSAxLjA7CgkJY292ZXJhZ2UgPSBkaXN0czIueCAqIGRpc3RzMi55OwoJfQoJQGlmICgxID09IDIgfHwgMSA9PSAzKSAKCXsKCQljb3ZlcmFnZSA9IDEuMCAtIGNvdmVyYWdlOwoJfQoJcmV0dXJuIF9pbnB1dCAqIGNvdmVyYWdlOwp9CnZvaWQgbWFpbigpIAp7CgkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJZmxvYXQ0IGNpcmNsZUVkZ2U7CgljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CglmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IEFBUmVjdEVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","GFYQAAAAMAAGGADDACRQAAAAMAACHQDCABRQAI7CAMAAAAAAKQAAAAAAAIAAAAAQGIBAAAA":"BAAAAExTS1PfCwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0NCByYWRpaV9zZWxlY3RvcjsKaW4gZmxvYXQ0IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHM7CmluIGZsb2F0NCBhYV9ibG9hdF9hbmRfY292ZXJhZ2U7CmluIGZsb2F0NCBza2V3OwppbiBmbG9hdDIgdHJhbnNsYXRlOwppbiBmbG9hdDQgcmFkaWlfeDsKaW4gZmxvYXQ0IHJhZGlpX3k7CmluIGhhbGY0IGNvbG9yOwpmbGF0IG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQgYWFfYmxvYXRfbXVsdGlwbGllciA9IDA7CglmbG9hdDIgY29ybmVyID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy54eTsKCWZsb2F0MiByYWRpdXNfb3V0c2V0ID0gY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cy56dzsKCWZsb2F0MiBhYV9ibG9hdF9kaXJlY3Rpb24gPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UueHk7CglmbG9hdCBpc19saW5lYXJfY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UudzsKCWZsb2F0MiBwaXhlbGxlbmd0aCA9IGludmVyc2VzcXJ0KGZsb2F0Mihkb3Qoc2tldy54eiwgc2tldy54eiksIGRvdChza2V3Lnl3LCBza2V3Lnl3KSkpOwoJZmxvYXQ0IG5vcm1hbGl6ZWRfYXhpc19kaXJzID0gc2tldyAqIHBpeGVsbGVuZ3RoLnh5eHk7CglmbG9hdDIgYXhpc3dpZHRocyA9IChhYnMobm9ybWFsaXplZF9heGlzX2RpcnMueHkpICsgYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnp3KSk7CglmbG9hdDIgYWFfYmxvYXRyYWRpdXMgPSBheGlzd2lkdGhzICogcGl4ZWxsZW5ndGggKiAuNTsKCWZsb2F0NCByYWRpaV9hbmRfbmVpZ2hib3JzID0gcmFkaWlfc2VsZWN0b3IqIGZsb2F0NHg0KHJhZGlpX3gsIHJhZGlpX3ksIHJhZGlpX3gueXh3eiwgcmFkaWlfeS53enl4KTsKCWZsb2F0MiByYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMueHk7CglmbG9hdDIgbmVpZ2hib3JfcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnp3OwoJZmxvYXQgY292ZXJhZ2VfbXVsdGlwbGllciA9IDE7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2VfbXVsdGlwbGllciA9IDEgLyAobWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpKTsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCX0KCWZsb2F0IGNvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLno7CglpZiAoYW55KGxlc3NUaGFuKHJhZGlpLCBhYV9ibG9hdHJhZGl1cyAqIDEuNSkpKSAKCXsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCQlhYV9ibG9hdF9kaXJlY3Rpb24gPSBzaWduKGNvcm5lcik7CgkJaWYgKGNvdmVyYWdlID4gLjUpIAoJCXsKCQkJYWFfYmxvYXRfZGlyZWN0aW9uID0gLWFhX2Jsb2F0X2RpcmVjdGlvbjsKCQl9CgkJaXNfbGluZWFyX2NvdmVyYWdlID0gMTsKCX0KCWVsc2UgCgl7CgkJcmFkaWkgPSBjbGFtcChyYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJbmVpZ2hib3JfcmFkaWkgPSBjbGFtcChuZWlnaGJvcl9yYWRpaSwgcGl4ZWxsZW5ndGggKiAxLjUsIDIgLSBwaXhlbGxlbmd0aCAqIDEuNSk7CgkJZmxvYXQyIHNwYWNpbmcgPSAyIC0gcmFkaWkgLSBuZWlnaGJvcl9yYWRpaTsKCQlmbG9hdDIgZXh0cmFfcGFkID0gbWF4KHBpeGVsbGVuZ3RoICogLjA2MjUgLSBzcGFjaW5nLCBmbG9hdDIoMCkpOwoJCXJhZGlpIC09IGV4dHJhX3BhZCAqIC41OwoJfQoJZmxvYXQyIGFhX291dHNldCA9IGFhX2Jsb2F0X2RpcmVjdGlvbiAqIGFhX2Jsb2F0cmFkaXVzICogYWFfYmxvYXRfbXVsdGlwbGllcjsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglpZiAoY292ZXJhZ2UgPiAuNSkgCgl7CgkJaWYgKGFhX2Jsb2F0X2RpcmVjdGlvbi54ICE9IDAgJiYgdmVydGV4cG9zLnggKiBjb3JuZXIueCA8IDApIAoJCXsKCQkJZmxvYXQgYmFja3NldCA9IGFicyh2ZXJ0ZXhwb3MueCk7CgkJCXZlcnRleHBvcy54ID0gMDsKCQkJdmVydGV4cG9zLnkgKz0gYmFja3NldCAqIHNpZ24oY29ybmVyLnkpICogcGl4ZWxsZW5ndGgueS9waXhlbGxlbmd0aC54OwoJCQljb3ZlcmFnZSA9IChjb3ZlcmFnZSAtIC41KSAqIGFicyhjb3JuZXIueCkgLyAoYWJzKGNvcm5lci54KSArIGJhY2tzZXQpICsgLjU7CgkJfQoJCWlmIChhYV9ibG9hdF9kaXJlY3Rpb24ueSAhPSAwICYmIHZlcnRleHBvcy55ICogY29ybmVyLnkgPCAwKSAKCQl7CgkJCWZsb2F0IGJhY2tzZXQgPSBhYnModmVydGV4cG9zLnkpOwoJCQl2ZXJ0ZXhwb3MueSA9IDA7CgkJCXZlcnRleHBvcy54ICs9IGJhY2tzZXQgKiBzaWduKGNvcm5lci54KSAqIHBpeGVsbGVuZ3RoLngvcGl4ZWxsZW5ndGgueTsKCQkJY292ZXJhZ2UgPSAoY292ZXJhZ2UgLSAuNSkgKiBhYnMoY29ybmVyLnkpIC8gKGFicyhjb3JuZXIueSkgKyBiYWNrc2V0KSArIC41OwoJCX0KCX0KCWZsb2F0MngyIHNrZXdtYXRyaXggPSBmbG9hdDJ4Mihza2V3Lnh5LCBza2V3Lnp3KTsKCWZsb2F0MiBkZXZjb29yZCA9IHZlcnRleHBvcyAqIHNrZXdtYXRyaXggKyB0cmFuc2xhdGU7CglpZiAoMCAhPSBpc19saW5lYXJfY292ZXJhZ2UpIAoJewoJCXZhcmNjb29yZF9TdGFnZTAueHkgPSBmbG9hdDIoMCwgY292ZXJhZ2UgKiBjb3ZlcmFnZV9tdWx0aXBsaWVyKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAK0CAABmbGF0IGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7CmluIGZsb2F0MiB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CglvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJZmxvYXQgeF9wbHVzXzE9dmFyY2Nvb3JkX1N0YWdlMC54LCB5PXZhcmNjb29yZF9TdGFnZTAueTsKCWhhbGYgY292ZXJhZ2U7CglpZiAoMCA9PSB4X3BsdXNfMSkgCgl7CgkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJfQoJZWxzZSAKCXsKCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJZm4gPSBmbWEoeSx5LCBmbik7CgkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJY292ZXJhZ2UgPSAuNSAtIGhhbGYoZm4vZm53aWR0aCk7CgkJY292ZXJhZ2UgPSBjbGFtcChjb3ZlcmFnZSwgMCwgMSk7Cgl9Cgljb3ZlcmFnZSA9IChjb3ZlcmFnZSA+PSAuNSkgPyAxIDogMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGNvdmVyYWdlKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQCAACAAAABGAIEBSAAIAAAAAAAAAACUAAAAEAAAAAAAEERQAAAAA":"BAAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAADsAwAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZC54ID0gY2xhbXAoc3Vic2V0Q29vcmQueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC54LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBzdWJzZXRDb29yZC55OwoJaGFsZjQgdGV4dHVyZUNvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgY2xhbXBlZENvb3JkKTsKCXJldHVybiB0ZXh0dXJlQ29sb3I7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0KTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCWNvbnN0IGhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCW91dHB1dF9TdGFnZTEgPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","AWQAGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAIACEIAEAAAAAAAAAQAAAACQAQAEAAAAAKAMYAAABAAAAATAFIQCAAAAAAAAAAAAAAAFIAAAAIAAAAAAAIJDAA":"BAAAAExTS1MJAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQ0IHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IHVsb2NhbE1hdHJpeF9TdGFnZTAueHogKiBpblBvc2l0aW9uICsgdWxvY2FsTWF0cml4X1N0YWdlMC55dzsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAQEAAAAAAAABAQAABQAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMF9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBfY29vcmRzKS4wMDByOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCXJldHVybiBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMF9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzBfYzBfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKfQpoYWxmNCBEZXZpY2VTcGFjZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0LCBza19GcmFnQ29vcmQueHkpOwp9CmhhbGY0IEJsZW5kX1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgkvLyBCbGVuZCBtb2RlOiBEc3RJbiAoQ29tcG9zZS1PbmUgYmVoYXZpb3IpCglyZXR1cm4gYmxlbmRfZHN0X2luKERldmljZVNwYWNlRWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCgxKSksIF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQmxlbmRfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","AWAAGAAAQAAIXCEPAGGP777777777737AAAAAAAAAAAAAUABAAAAACAAAAAEBSAI":"BAAAAExTS1OzAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAACYCAABpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDQgY2lyY2xlRWRnZTsKCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","K4IAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAAAAAAAAVIRQYAAAAAAACAQAAAAGJCABAAAAAICAAAAABAGOAACAQAAAABQCVAEQAEAIAAAAAAAAAVAAAAAAAAQAAAABAMQCAAAAA":"BAAAAExTS1NjAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMV9jMCkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAAAcFAAAdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMDsKdW5pZm9ybSBoYWxmNCB1XzFfS2VybmVsX1N0YWdlMV9jMF9jMFs3XTsKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMV9jMF9jMF9jMDsKdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1N0YWdlMV9jMF9jMF9jMF9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwppbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMF9jMF9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJZmxvYXQyIGluQ29vcmQgPSBfY29vcmRzOwoJZmxvYXQyIHN1YnNldENvb3JkOwoJc3Vic2V0Q29vcmQueCA9IGluQ29vcmQueDsKCXN1YnNldENvb3JkLnkgPSBpbkNvb3JkLnk7CglmbG9hdDIgY2xhbXBlZENvb3JkOwoJY2xhbXBlZENvb3JkLnggPSBzdWJzZXRDb29yZC54OwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfU3RhZ2UxX2MwX2MwX2MwX2MwLnksIHVjbGFtcF9TdGFnZTFfYzBfYzBfYzBfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJcmV0dXJuIHRleHR1cmVDb2xvcjsKfQpoYWxmNCBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCwgZmxvYXQyIF9jb29yZHMpIAp7CglyZXR1cm4gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzBfYzBfYzAoX2lucHV0LCAoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwX2MwKSAqIF9jb29yZHMueHkxKS54eSk7Cn0KaGFsZjQgR2F1c3NpYW5Db252b2x1dGlvbl9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgXzJfY29sb3IgPSBoYWxmNCgwLjApOwoJZmxvYXQyIF8zX2Nvb3JkID0gKHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCAtIGZsb2F0MigoMTIuMCAqIHVfMF9JbmNyZW1lbnRfU3RhZ2UxX2MwX2MwKSkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMF0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMV0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbMl0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbM10udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNF0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0ueCkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0ueSkpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0ueikpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNV0udykpOwoJKF8zX2Nvb3JkICs9IGZsb2F0Mih1XzBfSW5jcmVtZW50X1N0YWdlMV9jMF9jMCkpOwoJKF8yX2NvbG9yICs9IChNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwX2MwX2MwKF9pbnB1dCwgXzNfY29vcmQpICogdV8xX0tlcm5lbF9TdGFnZTFfYzBfYzBbNl0ueCkpOwoJcmV0dXJuIF8yX2NvbG9yOwp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJcmV0dXJuIEdhdXNzaWFuQ29udm9sdXRpb25fU3RhZ2UxX2MwX2MwKF9pbnB1dCk7Cn0Kdm9pZCBtYWluKCkgCnsKCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7Cgljb25zdCBoYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","K4JAAAAAAAGAAAAAAIWP577774BSZ7X7777QCAAAAABAAAAAABBAMAHSEECQAAAAABIACAAAAAEAAAAAIDEAQAAAAA":"BAAAAExTS1MFAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MiBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAEBAAAAAAAAAQEAZQQAAHVuaWZvcm0gZmxvYXQ0IHVjaXJjbGVfU3RhZ2UxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmNsZUVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIHByZXZDZW50ZXI7CglmbG9hdCBwcmV2UmFkaXVzID0gLTEuMDAwMDAwOwoJaGFsZiBkOwoJQGlmICgxID09IDIgfHwgMSA9PSAzKSAKCXsKCQlkID0gaGFsZigobGVuZ3RoKCh1Y2lyY2xlX1N0YWdlMV9jMC54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1N0YWdlMV9jMC53KSAtIDEuMCkgKiB1Y2lyY2xlX1N0YWdlMV9jMC56KTsKCX0KCWVsc2UgCgl7CgkJZCA9IGhhbGYoKDEuMCAtIGxlbmd0aCgodWNpcmNsZV9TdGFnZTFfYzAueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TdGFnZTFfYzAudykpICogdWNpcmNsZV9TdGFnZTFfYzAueik7Cgl9CgloYWxmNCBpbnB1dENvbG9yID0gX2lucHV0OwoJQGlmICgxID09IDEgfHwgMSA9PSAzKSAKCXsKCQlyZXR1cm4gaW5wdXRDb2xvciAqIGNsYW1wKGQsIDAuMCwgMS4wKTsKCX0KCWVsc2UgCgl7CgkJcmV0dXJuIGQgPiAwLjUgPyBpbnB1dENvbG9yIDogaGFsZjQoMC4wKTsKCX0KfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCWZsb2F0MiB0ZXhDb29yZDsKCXRleENvb3JkID0gdmxvY2FsQ29vcmRfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB0ZXhDb29yZCkgKiBoYWxmNCgxKSkpOwoJY29uc3QgaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJb3V0cHV0X1N0YWdlMSA9IENpcmNsZUVmZmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","DIQAAAAAMAAAAABAYAROFA2CAIAAAABAAAAAAAAAAAACABUSA4AAAEAAAAAAAQAFAAAAAIAAAAAAAIJDAAAA":"BAAAAExTS1NQAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc1NpemVJbnZfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gdXNob3J0MiBpblRleHR1cmVDb29yZHM7Cm91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBmbG9hdCB2VGV4SW5kZXhfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBCaXRtYXBUZXh0CglpbnQgdGV4SWR4ID0gMDsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihpblRleHR1cmVDb29yZHMueCwgaW5UZXh0dXJlQ29vcmRzLnkpOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNTaXplSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSBmbG9hdCh0ZXhJZHgpOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChpblBvc2l0aW9uLnggLCBpblBvc2l0aW9uLnksIDAsIDEpOwp9CgABAQAAAAAAAAEBAMUDAAB1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gZmxvYXQgdlRleEluZGV4X1N0YWdlMDsKaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCXJldHVybiBfaW5wdXQgKiBhbHBoYTsKfQp2b2lkIG1haW4oKSAKewoJLy8gU3RhZ2UgMCwgQml0bWFwVGV4dAoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJaGFsZjQgdGV4Q29sb3I7Cgl7CgkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB2VGV4dHVyZUNvb3Jkc19TdGFnZTApLnJycnI7Cgl9CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSB0ZXhDb2xvcjsKCWhhbGY0IG91dHB1dF9TdGFnZTE7CglvdXRwdXRfU3RhZ2UxID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA="}} \ No newline at end of file diff --git a/test/fake/image_file_service.dart b/test/fake/image_file_service.dart index 4c4716ae4..88d6803e0 100644 --- a/test/fake/image_file_service.dart +++ b/test/fake/image_file_service.dart @@ -7,14 +7,14 @@ import 'media_store_service.dart'; class FakeImageFileService extends Fake implements ImageFileService { @override - Future rename(AvesEntry entry, String newName) { + Future> rename(AvesEntry entry, String newName) { final contentId = FakeMediaStoreService.nextContentId; return SynchronousFuture({ 'uri': 'content://media/external/images/media/$contentId', 'contentId': contentId, 'path': '${entry.directory}/$newName', 'displayName': newName, - 'title': newName.substring(0, newName.length - entry.extension.length), + 'title': newName.substring(0, newName.length - entry.extension!.length), 'dateModifiedSecs': FakeMediaStoreService.dateSecs, }); } diff --git a/test/fake/media_store_service.dart b/test/fake/media_store_service.dart index fce6490e6..2fc34b787 100644 --- a/test/fake/media_store_service.dart +++ b/test/fake/media_store_service.dart @@ -12,7 +12,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService { Future> checkObsoleteContentIds(List knownContentIds) => SynchronousFuture([]); @override - Future> checkObsoletePaths(Map knownPathById) => SynchronousFuture([]); + Future> checkObsoletePaths(Map knownPathById) => SynchronousFuture([]); @override Stream getEntries(Map knownEntries) => Stream.fromIterable(entries); @@ -52,7 +52,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService { 'deletedSource': true, 'uri': 'content://media/external/images/media/$newContentId', 'contentId': newContentId, - 'path': entry.path.replaceFirst(sourceAlbum, destinationAlbum), + 'path': entry.path!.replaceFirst(sourceAlbum, destinationAlbum), 'displayName': '${entry.filenameWithoutExtension}${entry.extension}', 'title': entry.filenameWithoutExtension, 'dateModifiedSecs': FakeMediaStoreService.dateSecs, diff --git a/test/fake/metadata_db.dart b/test/fake/metadata_db.dart index 6b0d8b0a5..3c738bbce 100644 --- a/test/fake/metadata_db.dart +++ b/test/fake/metadata_db.dart @@ -1,6 +1,7 @@ import 'package:aves/model/covers.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:flutter/foundation.dart'; @@ -8,19 +9,19 @@ import 'package:flutter_test/flutter_test.dart'; class FakeMetadataDb extends Fake implements MetadataDb { @override - Future init() => null; + Future init() => SynchronousFuture(true); @override - Future removeIds(Set contentIds, {@required bool metadataOnly}) => null; + Future removeIds(Set contentIds, {required bool metadataOnly}) => SynchronousFuture(true); @override Future> loadEntries() => SynchronousFuture({}); @override - Future saveEntries(Iterable entries) => null; + Future saveEntries(Iterable entries) => SynchronousFuture(true); @override - Future updateEntryId(int oldId, AvesEntry entry) => null; + Future updateEntryId(int oldId, AvesEntry entry) => SynchronousFuture(true); @override Future> loadDates() => SynchronousFuture([]); @@ -29,38 +30,38 @@ class FakeMetadataDb extends Fake implements MetadataDb { Future> loadMetadataEntries() => SynchronousFuture([]); @override - Future saveMetadata(Iterable metadataEntries) => null; + Future saveMetadata(Set metadataEntries) => SynchronousFuture(true); @override - Future updateMetadataId(int oldId, CatalogMetadata metadata) => null; + Future updateMetadataId(int oldId, CatalogMetadata? metadata) => SynchronousFuture(true); @override Future> loadAddresses() => SynchronousFuture([]); @override - Future updateAddressId(int oldId, AddressDetails address) => null; + Future updateAddressId(int oldId, AddressDetails? address) => SynchronousFuture(true); @override Future> loadFavourites() => SynchronousFuture({}); @override - Future addFavourites(Iterable rows) => null; + Future addFavourites(Iterable rows) => SynchronousFuture(true); @override - Future updateFavouriteId(int oldId, FavouriteRow row) => null; + Future updateFavouriteId(int oldId, FavouriteRow row) => SynchronousFuture(true); @override - Future removeFavourites(Iterable rows) => null; + Future removeFavourites(Iterable rows) => SynchronousFuture(true); @override Future> loadCovers() => SynchronousFuture({}); @override - Future addCovers(Iterable rows) => null; + Future addCovers(Iterable rows) => SynchronousFuture(true); @override - Future updateCoverEntryId(int oldId, CoverRow row) => null; + Future updateCoverEntryId(int oldId, CoverRow row) => SynchronousFuture(true); @override - Future removeCovers(Iterable rows) => null; + Future removeCovers(Set filters) => SynchronousFuture(true); } diff --git a/test/fake/metadata_service.dart b/test/fake/metadata_service.dart index 766f27caa..684e41d9d 100644 --- a/test/fake/metadata_service.dart +++ b/test/fake/metadata_service.dart @@ -1,9 +1,10 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/services/metadata_service.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; class FakeMetadataService extends Fake implements MetadataService { @override - Future getCatalogMetadata(AvesEntry entry, {bool background = false}) => null; + Future getCatalogMetadata(AvesEntry entry, {bool background = false}) => SynchronousFuture(null); } diff --git a/test/fake/storage_service.dart b/test/fake/storage_service.dart index e8dddc162..e92b3460f 100644 --- a/test/fake/storage_service.dart +++ b/test/fake/storage_service.dart @@ -12,17 +12,19 @@ class FakeStorageService extends Fake implements StorageService { @override Future> getStorageVolumes() => SynchronousFuture({ - StorageVolume( + const StorageVolume( path: primaryPath, description: primaryDescription, isPrimary: true, isRemovable: false, + state: 'fake', ), - StorageVolume( + const StorageVolume( path: removablePath, description: removableDescription, isPrimary: false, isRemovable: true, + state: 'fake', ), }); } diff --git a/test/geo/countries_test.dart b/test/geo/countries_test.dart index e81d5e7c2..c1dda9647 100644 --- a/test/geo/countries_test.dart +++ b/test/geo/countries_test.dart @@ -7,7 +7,7 @@ void main() { // [lng, lat, z] const buenosAires = [-58.381667, -34.603333]; const paris = [2.348777, 48.875683]; - const seoul = [126.99, 37.56, 42]; + const seoul = [126.99, 37.56, 42.0]; const argentinaN3String = '032'; const franceN3String = '250'; const southKoreaN3String = '410'; @@ -15,10 +15,13 @@ void main() { test('Parse countries', () async { TestWidgetsFlutterBinding.ensureInitialized(); final topo = await countryTopology.getTopology(); + expect(topo != null, true); + if (topo == null) return; + final countries = topo.objects['countries'] as GeometryCollection; final argentina = countries.geometries.firstWhere((geometry) => geometry.id == argentinaN3String); - expect(argentina.properties['name'], 'Argentina'); + expect(argentina.properties!['name'], 'Argentina'); expect(argentina.containsPoint(topo, buenosAires), true); expect(argentina.containsPoint(topo, seoul), false); }); diff --git a/test/geo/topojson_test.dart b/test/geo/topojson_test.dart index 124435d7b..9606a3e7f 100644 --- a/test/geo/topojson_test.dart +++ b/test/geo/topojson_test.dart @@ -92,6 +92,9 @@ void main() { test('parse example', () async { final topo = await TopoJson().parse(example1); + expect(topo != null, true); + if (topo == null) return; + expect(topo.objects.containsKey('example'), true); final exampleObj = topo.objects['example'] as GeometryCollection; @@ -105,12 +108,15 @@ void main() { final polygon = exampleObj.geometries[2] as Polygon; expect(polygon.arcs.first, [-2]); - expect(polygon.properties.containsKey('prop0'), true); + expect(polygon.properties!.containsKey('prop0'), true); }); test('parse quantized example', () async { final topo = await TopoJson().parse(example1Quantized); + expect(topo != null, true); + if (topo == null) return; + expect(topo.arcs.first.first, [4000, 0]); - expect(topo.transform.scale, [0.0005000500050005, 0.00010001000100010001]); + expect(topo.transform!.scale, [0.0005000500050005, 0.00010001000100010001]); }); } diff --git a/test/model/collection_source_test.dart b/test/model/collection_source_test.dart index 874a128f1..b2caa0fe7 100644 --- a/test/model/collection_source_test.dart +++ b/test/model/collection_source_test.dart @@ -1,3 +1,4 @@ +// @dart=2.9 import 'dart:async'; import 'package:aves/model/availability.dart'; @@ -93,7 +94,7 @@ void main() { expect(source.rawAlbums.length, 1); expect(covers.count, 0); - final albumFilter = AlbumFilter(testAlbum, 'whatever'); + const albumFilter = AlbumFilter(testAlbum, 'whatever'); expect(albumFilter.test(image1), true); expect(covers.count, 0); expect(covers.coverContentId(albumFilter), null); @@ -115,7 +116,7 @@ void main() { final source = await _initSource(); await image1.toggleFavourite(); - final albumFilter = AlbumFilter(testAlbum, 'whatever'); + const albumFilter = AlbumFilter(testAlbum, 'whatever'); await covers.set(albumFilter, image1.contentId); await source.renameEntry(image1, 'image1b.jpg'); @@ -153,8 +154,8 @@ void main() { expect(source.rawAlbums.contains(sourceAlbum), true); expect(source.rawAlbums.contains(destinationAlbum), false); - final sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever'); - final destinationAlbumFilter = AlbumFilter(destinationAlbum, 'whatever'); + const sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever'); + const destinationAlbumFilter = AlbumFilter(destinationAlbum, 'whatever'); expect(sourceAlbumFilter.test(image1), true); expect(destinationAlbumFilter.test(image1), false); @@ -204,7 +205,7 @@ void main() { final source = await _initSource(); expect(source.rawAlbums.length, 1); - final sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever'); + const sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever'); await covers.set(sourceAlbumFilter, image1.contentId); await source.updateAfterMove( @@ -229,14 +230,14 @@ void main() { final source = await _initSource(); await image1.toggleFavourite(); - var albumFilter = AlbumFilter(sourceAlbum, 'whatever'); + var albumFilter = const AlbumFilter(sourceAlbum, 'whatever'); await covers.set(albumFilter, image1.contentId); await source.renameAlbum(sourceAlbum, destinationAlbum, { image1 }, { FakeMediaStoreService.moveOpEventFor(image1, sourceAlbum, destinationAlbum), }); - albumFilter = AlbumFilter(destinationAlbum, 'whatever'); + albumFilter = const AlbumFilter(destinationAlbum, 'whatever'); expect(favourites.count, 1); expect(image1.isFavourite, true); @@ -273,7 +274,7 @@ void main() { expect(source.getAlbumDisplayName(context, '${FakeStorageService.primaryPath}Seneca'), 'Seneca'); expect(source.getAlbumDisplayName(context, '${FakeStorageService.removablePath}Pictures/Cicero'), 'Cicero'); expect(source.getAlbumDisplayName(context, '${FakeStorageService.removablePath}Marcus Aurelius'), 'Marcus Aurelius'); - return Placeholder(); + return const Placeholder(); }, ), ); diff --git a/test/model/filters_test.dart b/test/model/filters_test.dart index 2e12a3422..9f475817d 100644 --- a/test/model/filters_test.dart +++ b/test/model/filters_test.dart @@ -1,3 +1,4 @@ +// @dart=2.9 import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; @@ -12,7 +13,7 @@ void main() { test('Filter serialization', () { CollectionFilter jsonRoundTrip(filter) => CollectionFilter.fromJson(filter.toJson()); - final album = AlbumFilter('path/to/album', 'album'); + const album = AlbumFilter('path/to/album', 'album'); expect(album, jsonRoundTrip(album)); const fav = FavouriteFilter.instance; diff --git a/test/widget_test.dart b/test/widget_test.dart index abe19491a..db070e29b 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,4 +1,4 @@ -import 'package:aves/main.dart'; +import 'package:aves/widgets/aves_app.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/test_driver/app.dart b/test_driver/app.dart index 23aaba037..34dab6200 100644 --- a/test_driver/app.dart +++ b/test_driver/app.dart @@ -1,3 +1,4 @@ +// @dart=2.9 import 'dart:ui'; import 'package:aves/main.dart' as app; @@ -15,7 +16,9 @@ void main() { // scan files copied from test assets // we do it via the app instead of broadcasting via ADB // because `MEDIA_SCANNER_SCAN_FILE` intent got deprecated in API 29 - PlatformStorageService().scanFile(p.join(targetPicturesDir, 'ipse.jpg'), 'image/jpeg'); + final storageService = PlatformStorageService(); + storageService.scanFile(p.join(targetPicturesDir, 'aves_logo.svg'), 'image/svg+xml'); + storageService.scanFile(p.join(targetPicturesDir, 'ipse.jpg'), 'image/jpeg'); configureAndLaunch(); } @@ -24,7 +27,8 @@ Future configureAndLaunch() async { await settings.init(); settings.keepScreenOn = KeepScreenOn.always; settings.hasAcceptedTerms = false; - settings.locale = Locale('en'); + settings.locale = const Locale('en'); + settings.homePage = HomePageSetting.collection; app.main(); } diff --git a/test_driver/app_test.dart b/test_driver/app_test.dart index a79313cd2..13f442cde 100644 --- a/test_driver/app_test.dart +++ b/test_driver/app_test.dart @@ -8,7 +8,7 @@ import 'constants.dart'; import 'utils/adb_utils.dart'; import 'utils/driver_extension.dart'; -FlutterDriver driver; +late FlutterDriver driver; void main() { group('[Aves app]', () { @@ -26,7 +26,7 @@ void main() { tearDownAll(() async { await removeDirectory(targetPicturesDir); - unawaited(driver?.close()); + unawaited(driver.close()); }); agreeToTerms(); @@ -37,29 +37,30 @@ void main() { selectFirstAlbum(); searchAlbum(); showViewer(); + goToNextImage(); toggleOverlay(); zoom(); showInfoMetadata(); scrollOffImage(); test('contemplation', () async { - await Future.delayed(Duration(seconds: 5)); + await Future.delayed(const Duration(seconds: 5)); }); - }, timeout: Timeout(Duration(seconds: 30))); + }, timeout: const Timeout(Duration(seconds: 30))); } void agreeToTerms() { test('[welcome] agree to terms', () async { - await driver.scroll(find.text('Terms of Service'), 0, -300, Duration(milliseconds: 500)); + await driver.scroll(find.text('Terms of Service'), 0, -300, const Duration(milliseconds: 500)); await driver.tap(find.byValueKey('agree-checkbox')); - await Future.delayed(Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); await driver.tap(find.byValueKey('continue-button')); await driver.waitUntilNoTransientCallbacks(); // wait for collection loading - await driver.waitForCondition(NoPendingPlatformMessages()); + await driver.waitForCondition(const NoPendingPlatformMessages()); }); } @@ -68,7 +69,7 @@ void visitAbout() { await driver.tap(find.byValueKey('appbar-leading-button')); await driver.waitUntilNoTransientCallbacks(); - await driver.tap(find.byValueKey('About-tile')); + await driver.tap(find.byValueKey('drawer-about-button')); await driver.waitUntilNoTransientCallbacks(); await pressDeviceBackButton(); @@ -77,11 +78,11 @@ void visitAbout() { } void visitSettings() { - test('[collection] visit about page', () async { + test('[collection] visit settings page', () async { await driver.tap(find.byValueKey('appbar-leading-button')); await driver.waitUntilNoTransientCallbacks(); - await driver.tap(find.byValueKey('Settings-tile')); + await driver.tap(find.byValueKey('drawer-settings-button')); await driver.waitUntilNoTransientCallbacks(); await pressDeviceBackButton(); @@ -124,7 +125,7 @@ void selectFirstAlbum() { await driver.waitUntilNoTransientCallbacks(); // wait for collection loading - await driver.waitForCondition(NoPendingPlatformMessages()); + await driver.waitForCondition(const NoPendingPlatformMessages()); await driver.tap(find.descendant( of: find.byValueKey('filter-grid-page'), @@ -154,9 +155,21 @@ void searchAlbum() { void showViewer() { test('[collection] show viewer', () async { - await driver.tap(find.byType('DecoratedThumbnail')); + await driver.tap(find.descendant( + of: find.byValueKey('collection-grid'), + matching: find.byType('DecoratedThumbnail'), + firstMatchOnly: true, + )); await driver.waitUntilNoTransientCallbacks(); - await Future.delayed(Duration(seconds: 2)); + await Future.delayed(const Duration(seconds: 2)); + }); +} + +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 Future.delayed(const Duration(seconds: 2)); }); } @@ -166,11 +179,11 @@ void toggleOverlay() { print('* hide overlay'); await driver.tap(imageView); - await Future.delayed(Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); print('* show overlay'); await driver.tap(imageView); - await Future.delayed(Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); }); } @@ -179,13 +192,13 @@ void zoom() { final imageView = find.byValueKey('imageview'); await driver.doubleTap(imageView); - await Future.delayed(Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); await driver.doubleTap(imageView); - await Future.delayed(Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); await driver.doubleTap(imageView); - await Future.delayed(Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); }); } @@ -194,12 +207,12 @@ void showInfoMetadata() { final verticalPageView = find.byValueKey('vertical-pageview'); print('* scroll down to info'); - await driver.scroll(verticalPageView, 0, -600, Duration(milliseconds: 400)); - await Future.delayed(Duration(seconds: 2)); + await driver.scroll(verticalPageView, 0, -600, const Duration(milliseconds: 400)); + await Future.delayed(const Duration(seconds: 2)); print('* scroll down to metadata details'); - await driver.scroll(verticalPageView, 0, -800, Duration(milliseconds: 600)); - await Future.delayed(Duration(seconds: 1)); + await driver.scroll(verticalPageView, 0, -800, const Duration(milliseconds: 600)); + await Future.delayed(const Duration(seconds: 1)); print('* toggle GPS metadata'); final gpsTile = find.descendant( @@ -212,8 +225,8 @@ void showInfoMetadata() { await driver.waitUntilNoTransientCallbacks(); print('* scroll up to show app bar'); - await driver.scroll(verticalPageView, 0, 100, Duration(milliseconds: 400)); - await Future.delayed(Duration(seconds: 1)); + await driver.scroll(verticalPageView, 0, 100, const Duration(milliseconds: 400)); + await Future.delayed(const Duration(seconds: 1)); print('* back to image'); await driver.tap(find.byValueKey('back-button')); @@ -223,7 +236,7 @@ void showInfoMetadata() { void scrollOffImage() { test('[viewer] scroll off', () async { - await driver.scroll(find.byValueKey('imageview'), 0, 800, Duration(milliseconds: 600)); - await Future.delayed(Duration(seconds: 1)); + await driver.scroll(find.byValueKey('imageview'), 0, 800, const Duration(milliseconds: 600)); + await Future.delayed(const Duration(seconds: 1)); }); } diff --git a/test_driver/assets/aves_logo.svg b/test_driver/assets/aves_logo.svg new file mode 100644 index 000000000..dc161703a --- /dev/null +++ b/test_driver/assets/aves_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test_driver/utils/adb_utils.dart b/test_driver/utils/adb_utils.dart index 3377223e5..a663f6ea9 100644 --- a/test_driver/utils/adb_utils.dart +++ b/test_driver/utils/adb_utils.dart @@ -5,7 +5,7 @@ import 'package:path/path.dart' as p; String get adb { final env = Platform.environment; // e.g. C:\Users\\AppData\Local\Android\Sdk - final sdkDir = env['ANDROID_SDK_ROOT'] ?? env['ANDROID_SDK']; + final sdkDir = env['ANDROID_SDK_ROOT'] ?? env['ANDROID_SDK']!; return p.join(sdkDir, 'platform-tools', Platform.isWindows ? 'adb.exe' : 'adb'); } @@ -40,7 +40,7 @@ Future copyContent(String sourceDir, String targetDir) async { // only works in debug mode Future grantPermissions(String packageName, Iterable permissions) async { - await Future.forEach(permissions, (permission) => runAdb(['shell', 'pm', 'grant', packageName, permission])); + await Future.forEach(permissions, (permission) => runAdb(['shell', 'pm', 'grant', packageName, permission])); } Future pressDeviceBackButton() => runAdb(['shell', 'input', 'keyevent', 'KEYCODE_BACK']); diff --git a/test_driver/utils/driver_extension.dart b/test_driver/utils/driver_extension.dart index 9e10657c9..3d70e7569 100644 --- a/test_driver/utils/driver_extension.dart +++ b/test_driver/utils/driver_extension.dart @@ -3,7 +3,7 @@ import 'package:flutter_driver/flutter_driver.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 { + Future doubleTap(SerializableFinder finder, {Duration? timeout}) async { await tap(finder, timeout: timeout); await Future.delayed(doubleTapDelay); await tap(finder, timeout: timeout); diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index 3cbdf3c75..ba1f34c79 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,6 +1,6 @@ Thanks for using Aves! -v1.4.1: -- motion photo support -- handle share intent -- fixes to handle large MP4 and PSD files +v1.4.2: +- improved navigation usability +- changed thumbnail layout +- improved playing videos with non-square pixels Full changelog available on Github \ No newline at end of file